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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-14 11:41:52 +0300
commit585826cb22ecea5998a2c2a4675735c94bdeedac (patch)
tree5b05f0b30d33cef48963609e8a18a4dff260eab3 /app/models
parentdf221d036e5d0c6c0ee4d55b9c97f481ee05dee8 (diff)
Add latest changes from gitlab-org/gitlab@16-6-stable-eev16.6.0-rc42
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse_report.rb16
-rw-r--r--app/models/active_session.rb36
-rw-r--r--app/models/activity_pub.rb7
-rw-r--r--app/models/activity_pub/releases_subscription.rb22
-rw-r--r--app/models/ai/service_access_token.rb10
-rw-r--r--app/models/analytics/cycle_analytics/value_stream.rb6
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/application_setting_implementation.rb12
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/bulk_imports/failure.rb4
-rw-r--r--app/models/ci/bridge.rb4
-rw-r--r--app/models/ci/build.rb22
-rw-r--r--app/models/ci/build_trace_chunks/redis_base.rb6
-rw-r--r--app/models/ci/build_trace_metadata.rb2
-rw-r--r--app/models/ci/catalog/components_project.rb7
-rw-r--r--app/models/ci/catalog/listing.rb49
-rw-r--r--app/models/ci/catalog/resource.rb44
-rw-r--r--app/models/ci/catalog/resources/component.rb2
-rw-r--r--app/models/ci/catalog/resources/version.rb96
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/job_token/scope.rb5
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/ci/ref.rb17
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_manager.rb21
-rw-r--r--app/models/ci/sources/pipeline.rb2
-rw-r--r--app/models/ci/stage.rb5
-rw-r--r--app/models/commit.rb8
-rw-r--r--app/models/commit_status.rb32
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage_event_model.rb1
-rw-r--r--app/models/concerns/can_move_repository_storage.rb19
-rw-r--r--app/models/concerns/ci/has_status.rb11
-rw-r--r--app/models/concerns/commit_signature.rb1
-rw-r--r--app/models/concerns/diff_positionable_note.rb1
-rw-r--r--app/models/concerns/enums/package_metadata.rb3
-rw-r--r--app/models/concerns/enums/sbom.rb3
-rw-r--r--app/models/concerns/merge_request_reviewer_state.rb3
-rw-r--r--app/models/concerns/repository_storage_movable.rb27
-rw-r--r--app/models/concerns/restricted_signup.rb1
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb2
-rw-r--r--app/models/concerns/use_sql_function_for_primary_key_lookups.rb39
-rw-r--r--app/models/concerns/users/visitable.rb39
-rw-r--r--app/models/container_repository.rb31
-rw-r--r--app/models/deployment.rb3
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/group.rb93
-rw-r--r--app/models/guest.rb9
-rw-r--r--app/models/integration.rb21
-rw-r--r--app/models/integrations/apple_app_store.rb6
-rw-r--r--app/models/integrations/asana.rb6
-rw-r--r--app/models/integrations/assembla.rb4
-rw-r--r--app/models/integrations/bamboo.rb6
-rw-r--r--app/models/integrations/base_chat_notification.rb4
-rw-r--r--app/models/integrations/base_slack_notification.rb2
-rw-r--r--app/models/integrations/bugzilla.rb6
-rw-r--r--app/models/integrations/buildkite.rb12
-rw-r--r--app/models/integrations/campfire.rb6
-rw-r--r--app/models/integrations/clickup.rb6
-rw-r--r--app/models/integrations/confluence.rb4
-rw-r--r--app/models/integrations/custom_issue_tracker.rb6
-rw-r--r--app/models/integrations/datadog.rb6
-rw-r--r--app/models/integrations/discord.rb14
-rw-r--r--app/models/integrations/drone_ci.rb12
-rw-r--r--app/models/integrations/emails_on_push.rb4
-rw-r--r--app/models/integrations/ewm.rb6
-rw-r--r--app/models/integrations/external_wiki.rb14
-rw-r--r--app/models/integrations/gitlab_slack_application.rb4
-rw-r--r--app/models/integrations/google_play.rb6
-rw-r--r--app/models/integrations/hangouts_chat.rb6
-rw-r--r--app/models/integrations/harbor.rb26
-rw-r--r--app/models/integrations/irker.rb38
-rw-r--r--app/models/integrations/jenkins.rb6
-rw-r--r--app/models/integrations/jira.rb24
-rw-r--r--app/models/integrations/mattermost.rb6
-rw-r--r--app/models/integrations/mattermost_slash_commands.rb4
-rw-r--r--app/models/integrations/microsoft_teams.rb6
-rw-r--r--app/models/integrations/mock_ci.rb4
-rw-r--r--app/models/integrations/mock_monitoring.rb4
-rw-r--r--app/models/integrations/packagist.rb4
-rw-r--r--app/models/integrations/pipelines_email.rb4
-rw-r--r--app/models/integrations/pivotaltracker.rb6
-rw-r--r--app/models/integrations/prometheus.rb4
-rw-r--r--app/models/integrations/pumble.rb6
-rw-r--r--app/models/integrations/pushover.rb4
-rw-r--r--app/models/integrations/redmine.rb6
-rw-r--r--app/models/integrations/shimo.rb4
-rw-r--r--app/models/integrations/slack.rb4
-rw-r--r--app/models/integrations/slack_slash_commands.rb4
-rw-r--r--app/models/integrations/squash_tm.rb6
-rw-r--r--app/models/integrations/teamcity.rb6
-rw-r--r--app/models/integrations/telegram.rb6
-rw-r--r--app/models/integrations/unify_circuit.rb6
-rw-r--r--app/models/integrations/webex_teams.rb6
-rw-r--r--app/models/integrations/youtrack.rb6
-rw-r--r--app/models/integrations/zentao.rb8
-rw-r--r--app/models/member.rb11
-rw-r--r--app/models/members/members/members_with_parents.rb105
-rw-r--r--app/models/members/project_member.rb6
-rw-r--r--app/models/merge_request.rb58
-rw-r--r--app/models/merge_request_context_commit_diff_file.rb1
-rw-r--r--app/models/merge_request_diff_commit.rb10
-rw-r--r--app/models/ml/candidate.rb2
-rw-r--r--app/models/ml/model.rb14
-rw-r--r--app/models/ml/model_metadata.rb13
-rw-r--r--app/models/ml/model_version.rb19
-rw-r--r--app/models/namespace.rb31
-rw-r--r--app/models/namespace_setting.rb10
-rw-r--r--app/models/network/graph.rb20
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/organizations/organization.rb4
-rw-r--r--app/models/packages/npm/metadata_cache.rb6
-rw-r--r--app/models/packages/nuget/symbol.rb3
-rw-r--r--app/models/packages/protection/rule.rb8
-rw-r--r--app/models/packages/pypi/metadatum.rb16
-rw-r--r--app/models/packages/tag.rb7
-rw-r--r--app/models/pages/lookup_path.rb12
-rw-r--r--app/models/pages_deployment.rb17
-rw-r--r--app/models/pages_domain.rb22
-rw-r--r--app/models/personal_access_token.rb7
-rw-r--r--app/models/project.rb96
-rw-r--r--app/models/project_feature_usage.rb13
-rw-r--r--app/models/project_pages_metadatum.rb3
-rw-r--r--app/models/project_snippet.rb2
-rw-r--r--app/models/projects/repository_storage_move.rb5
-rw-r--r--app/models/protected_branch.rb1
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/service_desk/custom_email_credential.rb11
-rw-r--r--app/models/snippet.rb1
-rw-r--r--app/models/snippet_repository.rb5
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/system_note_metadata.rb1
-rw-r--r--app/models/upload.rb2
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/user_custom_attribute.rb1
-rw-r--r--app/models/user_detail.rb23
-rw-r--r--app/models/user_preference.rb14
-rw-r--r--app/models/users/anonymous.rb11
-rw-r--r--app/models/users/callout.rb7
-rw-r--r--app/models/users/credit_card_validation.rb6
-rw-r--r--app/models/users/group_visit.rb7
-rw-r--r--app/models/users/phone_number_validation.rb4
-rw-r--r--app/models/users/project_visit.rb7
-rw-r--r--app/models/vs_code/settings/vs_code_setting.rb4
-rw-r--r--app/models/wiki_page.rb7
-rw-r--r--app/models/work_item.rb17
146 files changed, 1224 insertions, 633 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 872dedf07b1..de6b644c536 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -139,11 +139,11 @@ class AbuseReport < ApplicationRecord
def reported_content
case report_type
when :issue
- project.issues.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.issues.iid_in(route_hash[:id]).pick(:description_html)
when :merge_request
- project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
+ reported_project.merge_requests.iid_in(route_hash[:id]).pick(:description_html)
when :comment
- project.notes.id_in(note_id_from_url).pick(:note_html)
+ reported_project.notes.id_in(note_id_from_url).pick(:note_html)
end
end
@@ -157,13 +157,19 @@ class AbuseReport < ApplicationRecord
user.abuse_reports.open.by_category(category).id_not_in(id).includes(:reporter)
end
+ # createNote mutation calls noteable.project,
+ # which in case of abuse reports is nil
+ def project
+ nil
+ end
+
private
- def project
+ def reported_project
Project.find_by_full_path(route_hash.values_at(:namespace_id, :project_id).join('/'))
end
- def group
+ def reported_group
Group.find_by_full_path(route_hash[:group_id])
end
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index e42f9eeef23..9756e1b7dd3 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -84,7 +84,7 @@ class ActiveSession
)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
+ Gitlab::Redis::CrossSlot::Pipeline.new(redis).pipelined do |pipeline|
pipeline.setex(
key_name(user.id, session_private_id),
expiry,
@@ -135,9 +135,15 @@ class ActiveSession
redis.srem(lookup_key_name(user.id), session_ids)
+ session_keys = rack_session_keys(session_ids)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(key_names)
- redis.del(rack_session_keys(session_ids))
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(key_names, redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(session_keys, redis)
+ else
+ redis.del(key_names)
+ redis.del(session_keys)
+ end
end
end
@@ -206,7 +212,13 @@ class ActiveSession
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.mget(session_keys_batch).compact.map do |raw_session|
+ raw_sessions = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(session_keys_batch, redis)
+ else
+ redis.mget(session_keys_batch)
+ end
+
+ raw_sessions.compact.map do |raw_session|
load_raw_session(raw_session)
end
end
@@ -249,7 +261,13 @@ class ActiveSession
found = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
- session_ids.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ session_ids.zip(entries).to_h
end
found.compact!
@@ -258,7 +276,13 @@ class ActiveSession
fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = missing.map { |session_id| key_name_v1(user_id, session_id) }
- missing.zip(redis.mget(entry_keys)).to_h
+ entries = if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_get(entry_keys, redis)
+ else
+ redis.mget(entry_keys)
+ end
+
+ missing.zip(entries).to_h
end
fallbacks.merge(found.compact)
diff --git a/app/models/activity_pub.rb b/app/models/activity_pub.rb
new file mode 100644
index 00000000000..9131d8be776
--- /dev/null
+++ b/app/models/activity_pub.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ def self.table_name_prefix
+ "activity_pub_"
+ end
+end
diff --git a/app/models/activity_pub/releases_subscription.rb b/app/models/activity_pub/releases_subscription.rb
new file mode 100644
index 00000000000..a6304f1fc35
--- /dev/null
+++ b/app/models/activity_pub/releases_subscription.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActivityPub
+ class ReleasesSubscription < ApplicationRecord
+ belongs_to :project, optional: false
+
+ enum :status, [:requested, :accepted], default: :requested
+
+ attribute :payload, Gitlab::Database::Type::JsonPgSafe.new
+
+ validates :payload, json_schema: { filename: 'activity_pub_follow_payload' }, allow_blank: true
+ validates :subscriber_url, presence: true, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: true
+ validates :subscriber_inbox_url, uniqueness: { case_sensitive: false, scope: :project_id },
+ public_url: { allow_nil: true }
+ validates :shared_inbox_url, public_url: { allow_nil: true }
+
+ def self.find_by_subscriber_url(subscriber_url)
+ find_by('LOWER(subscriber_url) = ?', subscriber_url.downcase)
+ end
+ end
+end
diff --git a/app/models/ai/service_access_token.rb b/app/models/ai/service_access_token.rb
index b8a2a271976..46dfbe9078c 100644
--- a/app/models/ai/service_access_token.rb
+++ b/app/models/ai/service_access_token.rb
@@ -2,11 +2,13 @@
module Ai
class ServiceAccessToken < ApplicationRecord
+ include IgnorableColumns
self.table_name = 'service_access_tokens'
+ ignore_column :category, remove_with: '16.8', remove_after: '2024-01-22'
+
scope :expired, -> { where('expires_at < :now', now: Time.current) }
scope :active, -> { where('expires_at > :now', now: Time.current) }
- scope :for_category, ->(category) { where(category: category) }
attr_encrypted :token,
mode: :per_attribute_iv,
@@ -16,11 +18,5 @@ module Ai
encode_iv: false
validates :token, :expires_at, presence: true
-
- enum category: {
- code_suggestions: 1
- }
-
- validates :category, presence: true
end
end
diff --git a/app/models/analytics/cycle_analytics/value_stream.rb b/app/models/analytics/cycle_analytics/value_stream.rb
index 7f8c6eef704..d884932072b 100644
--- a/app/models/analytics/cycle_analytics/value_stream.rb
+++ b/app/models/analytics/cycle_analytics/value_stream.rb
@@ -36,6 +36,12 @@ module Analytics
new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, namespace: namespace)
end
+ def project
+ return unless namespace.is_a?(::Namespaces::ProjectNamespace)
+
+ namespace.project
+ end
+
private
def max_value_streams_count
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 824a2bd9fa4..8d4f50de75e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -30,7 +30,9 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
jitsu_project_xid
jitsu_administrator_email
], remove_with: '16.5', remove_after: '2023-09-22'
- ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.6', remove_after: '2023-10-22'
+ ignore_columns %i[encrypted_ai_access_token encrypted_ai_access_token_iv], remove_with: '16.10', remove_after: '2024-03-22'
+
+ ignore_columns %i[repository_storages], remove_with: '16.8', remove_after: '2023-12-21'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -91,7 +93,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
serialize :domain_denylist, Array # rubocop:disable Cop/ActiveRecordSerialize
- serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize
# See https://gitlab.com/gitlab-org/gitlab/-/issues/300916
serialize :asset_proxy_allowlist, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -303,8 +304,6 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
- validates :repository_storages, presence: true
- validate :check_repository_storages
validate :check_repository_storages_weighted
validates :auto_devops_domain,
@@ -488,7 +487,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile,
+ validates :invitation_flow_enforcement, :can_create_group, :allow_project_creation_for_guest_and_below, :user_defaults_to_private_profile,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 1bd15a56de5..00b093c8ac3 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -57,6 +57,7 @@ module ApplicationSettingImplementation
default_artifacts_expire_in: '30 days',
default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_branch_protection_defaults: Settings.gitlab['default_branch_protection_defaults'],
default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
@@ -158,7 +159,6 @@ module ApplicationSettingImplementation
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages_weighted: { 'default' => 100 },
- repository_storages: ['default'],
require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
@@ -433,10 +433,6 @@ module ApplicationSettingImplementation
read_attribute(:asset_proxy_whitelist)
end
- def repository_storages
- Array(read_attribute(:repository_storages))
- end
-
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
@@ -644,12 +640,6 @@ module ApplicationSettingImplementation
self.uuid = SecureRandom.uuid
end
- def check_repository_storages
- invalid = repository_storages - Gitlab.config.repositories.storages.keys
- errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless
- invalid.empty?
- end
-
def coerce_repository_storages_weighted
repository_storages_weighted.transform_values!(&:to_i)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 437118c36e8..a075c2f7e4f 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -124,6 +124,10 @@ class BulkImports::Entity < ApplicationRecord
entity_type.pluralize
end
+ def portable_class
+ entity_type.classify.constantize
+ end
+
def base_resource_url_path
"/#{pluralized_name}/#{encoded_source_full_path}"
end
diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb
index 44d16618c77..8a6077b523c 100644
--- a/app/models/bulk_imports/failure.rb
+++ b/app/models/bulk_imports/failure.rb
@@ -15,6 +15,10 @@ class BulkImports::Failure < ApplicationRecord
pipeline_relation || default_relation
end
+ def exception_message=(message)
+ super(::Projects::ImportErrorFilter.filter_message(message).truncate(255))
+ end
+
private
def pipeline_relation
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d0ccf5c543a..cf6401dc1da 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -114,7 +114,7 @@ module Ci
project = options&.dig(:trigger, :project)
next unless project
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(project, all_variables)
end
end
@@ -199,7 +199,7 @@ module Ci
branch = options&.dig(:trigger, :branch)
return unless branch
- scoped_variables.to_runner_variables.yield_self do |all_variables|
+ scoped_variables.to_runner_variables.then do |all_variables|
::ExpandVariables.expand(branch, all_variables)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d2cf9058976..0bb93a68470 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -392,8 +392,8 @@ module Ci
name == 'pages'
end
- # overridden on EE
- def pages_path_prefix; end
+ # Overriden on EE
+ def pages; end
def runnable?
true
@@ -729,7 +729,7 @@ module Ci
end
def artifacts_expired?
- artifacts_expire_at && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def artifacts_expire_in
@@ -745,7 +745,7 @@ module Ci
def has_expired_locked_archive_artifacts?
locked_artifacts? &&
- artifacts_expire_at.present? && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def has_expiring_archive_artifacts?
@@ -921,13 +921,25 @@ module Ci
# Consider this object to have a structural integrity problems
def doom!
transaction do
- update_columns(status: :failed, failure_reason: :data_integrity_failure)
+ now = Time.current
+ attributes = {
+ status: :failed,
+ failure_reason: :data_integrity_failure,
+ updated_at: now
+ }
+ attributes[:finished_at] = now unless finished_at.present?
+
+ update_columns(attributes)
all_queuing_entries.delete_all
all_runtime_metadata.delete_all
end
deployment&.sync_status_with(self)
+ ::Gitlab::Ci::Pipeline::Metrics
+ .job_failure_reason_counter
+ .increment(reason: :data_integrity_failure)
+
Gitlab::AppLogger.info(
message: 'Build doomed',
class: self.class.name,
diff --git a/app/models/ci/build_trace_chunks/redis_base.rb b/app/models/ci/build_trace_chunks/redis_base.rb
index 3b7a844d122..5f6b5c30a6a 100644
--- a/app/models/ci/build_trace_chunks/redis_base.rb
+++ b/app/models/ci/build_trace_chunks/redis_base.rb
@@ -71,7 +71,11 @@ module Ci
with_redis do |redis|
# https://gitlab.com/gitlab-org/gitlab/-/issues/224171
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.del(keys)
+ if Gitlab::Redis::ClusterUtil.cluster?(redis)
+ Gitlab::Redis::ClusterUtil.batch_unlink(keys, redis)
+ else
+ redis.del(keys)
+ end
end
end
end
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index c5ad3d19425..525cb08f2ca 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -33,7 +33,7 @@ module Ci
return false unless archival_attempts_available?
return true unless last_archival_attempt_at
- last_archival_attempt_at + backoff < Time.current
+ (last_archival_attempt_at + backoff).past?
end
def archival_attempts_available?
diff --git a/app/models/ci/catalog/components_project.rb b/app/models/ci/catalog/components_project.rb
index 2bc33a6f050..02593d41bc2 100644
--- a/app/models/ci/catalog/components_project.rb
+++ b/app/models/ci/catalog/components_project.rb
@@ -9,7 +9,8 @@ module Ci
TEMPLATE_FILE = 'template.yml'
TEMPLATES_DIR = 'templates'
- TEMPLATE_PATH_REGEX = '^templates\/\w+\-?\w+(?:\/template)?\.yml$'
+ TEMPLATE_PATH_REGEX = '^templates\/[\w-]+(?:\/template)?\.yml$'
+ COMPONENTS_LIMIT = 10
ComponentData = Struct.new(:content, :path, keyword_init: true)
@@ -18,8 +19,8 @@ module Ci
@sha = sha
end
- def fetch_component_paths(sha)
- project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha)
+ def fetch_component_paths(sha, limit: COMPONENTS_LIMIT)
+ project.repository.search_files_by_regexp(TEMPLATE_PATH_REGEX, sha, limit: limit)
end
def extract_component_name(path)
diff --git a/app/models/ci/catalog/listing.rb b/app/models/ci/catalog/listing.rb
index c3b18af8c3f..51bd85016a5 100644
--- a/app/models/ci/catalog/listing.rb
+++ b/app/models/ci/catalog/listing.rb
@@ -3,42 +3,53 @@
module Ci
module Catalog
class Listing
- # This class is the SSoT to displaying the list of resources in the
- # CI/CD Catalog given a namespace as a scope.
+ # This class is the SSoT to displaying the list of resources in the CI/CD Catalog.
# This model is not directly backed by a table and joins catalog resources
# with projects to return relevant data.
- def initialize(namespace, current_user)
- raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
- @namespace = namespace
+ MIN_SEARCH_LENGTH = 3
+
+ def initialize(current_user)
@current_user = current_user
end
- def resources(sort: nil)
+ def resources(namespace: nil, sort: nil, search: nil)
+ relation = all_resources
+ relation = by_namespace(relation, namespace)
+ relation = by_search(relation, search)
+
case sort.to_s
- when 'name_desc' then all_resources.order_by_name_desc
- when 'name_asc' then all_resources.order_by_name_asc
- when 'latest_released_at_desc' then all_resources.order_by_latest_released_at_desc
- when 'latest_released_at_asc' then all_resources.order_by_latest_released_at_asc
+ when 'name_desc' then relation.order_by_name_desc
+ when 'name_asc' then relation.order_by_name_asc
+ when 'latest_released_at_desc' then relation.order_by_latest_released_at_desc
+ when 'latest_released_at_asc' then relation.order_by_latest_released_at_asc
+ when 'created_at_asc' then relation.order_by_created_at_asc
else
- all_resources.order_by_created_at_desc
+ relation.order_by_created_at_desc
end
end
private
- attr_reader :namespace, :current_user
+ attr_reader :current_user
def all_resources
- Ci::Catalog::Resource
- .joins(:project).includes(:project)
- .merge(projects_in_namespace_visible_to_user)
+ Ci::Catalog::Resource.joins(:project).includes(:project)
+ .merge(Project.public_or_visible_to_user(current_user))
+ end
+
+ def by_namespace(relation, namespace)
+ return relation unless namespace
+ raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
+
+ relation.merge(Project.in_namespace(namespace.self_and_descendant_ids))
end
- def projects_in_namespace_visible_to_user
- Project
- .in_namespace(namespace.self_and_descendant_ids)
- .public_or_visible_to_user(current_user, ::Gitlab::Access::DEVELOPER)
+ def by_search(relation, search)
+ return relation unless search
+ return relation.none if search.length < MIN_SEARCH_LENGTH
+
+ relation.search(search)
end
end
end
diff --git a/app/models/ci/catalog/resource.rb b/app/models/ci/catalog/resource.rb
index 8ffc0292a69..f947c5158cf 100644
--- a/app/models/ci/catalog/resource.rb
+++ b/app/models/ci/catalog/resource.rb
@@ -8,29 +8,55 @@ module Ci
# dependency on the Project model and its need to join with that table
# in order to generate the CI/CD catalog.
class Resource < ::ApplicationRecord
+ include Gitlab::SQL::Pattern
+
self.table_name = 'catalog_resources'
belongs_to :project
- has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :catalog_resource
- has_many :versions, class_name: 'Ci::Catalog::Resources::Version', inverse_of: :catalog_resource
+ has_many :components, class_name: 'Ci::Catalog::Resources::Component', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
+ has_many :versions, class_name: 'Ci::Catalog::Resources::Version', foreign_key: :catalog_resource_id,
+ inverse_of: :catalog_resource
scope :for_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :search, ->(query) { fuzzy_search(query, [:name, :description], use_minimum_char_limit: false) }
+
scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
- scope :order_by_name_desc, -> { joins(:project).merge(Project.sorted_by_name_desc) }
- scope :order_by_name_asc, -> { joins(:project).merge(Project.sorted_by_name_asc) }
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_name_desc, -> { reorder(arel_table[:name].desc.nulls_last) }
+ scope :order_by_name_asc, -> { reorder(arel_table[:name].asc.nulls_last) }
scope :order_by_latest_released_at_desc, -> { reorder(arel_table[:latest_released_at].desc.nulls_last) }
scope :order_by_latest_released_at_asc, -> { reorder(arel_table[:latest_released_at].asc.nulls_last) }
- delegate :avatar_path, :description, :name, :star_count, :forks_count, to: :project
+ delegate :avatar_path, :star_count, :forks_count, to: :project
enum state: { draft: 0, published: 1 }
- def versions
- project.releases.order_released_desc
+ before_create :sync_with_project
+
+ def unpublish!
+ update!(state: :draft)
+ end
+
+ def publish!
+ update!(state: :published)
+ end
+
+ def sync_with_project!
+ sync_with_project
+ save!
end
- def latest_version
- project.releases.latest
+ private
+
+ # These columns are denormalized from the `projects` table. We first sync these
+ # columns when the catalog resource record is created. Then any updates to the
+ # `projects` columns will be synced to the `catalog_resources` table by a worker
+ # (to be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/429376.)
+ def sync_with_project
+ self.name = project.name
+ self.description = project.description
+ self.visibility_level = project.visibility_level
end
end
end
diff --git a/app/models/ci/catalog/resources/component.rb b/app/models/ci/catalog/resources/component.rb
index 7b95c14ba7e..07d5404981b 100644
--- a/app/models/ci/catalog/resources/component.rb
+++ b/app/models/ci/catalog/resources/component.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource component.
# The data will be used as metadata of a component.
class Component < ::ApplicationRecord
+ include BulkInsertSafe
+
self.table_name = 'catalog_resource_components'
belongs_to :project, inverse_of: :ci_components
diff --git a/app/models/ci/catalog/resources/version.rb b/app/models/ci/catalog/resources/version.rb
index 68f60e6a965..bd0ebc77a6d 100644
--- a/app/models/ci/catalog/resources/version.rb
+++ b/app/models/ci/catalog/resources/version.rb
@@ -6,6 +6,8 @@ module Ci
# This class represents a CI/CD Catalog resource version.
# Only versions which contain valid CI components are included in this table.
class Version < ::ApplicationRecord
+ include BulkInsertableAssociations
+
self.table_name = 'catalog_resource_versions'
belongs_to :release, inverse_of: :catalog_resource_version
@@ -14,6 +16,100 @@ module Ci
has_many :components, class_name: 'Ci::Catalog::Resources::Component', inverse_of: :version
validates :release, :catalog_resource, :project, presence: true
+
+ scope :for_catalog_resources, ->(catalog_resources) { where(catalog_resource_id: catalog_resources) }
+ scope :preloaded, -> { includes(:catalog_resource, project: [:route, { namespace: :route }], release: :author) }
+
+ scope :order_by_created_at_asc, -> { reorder(created_at: :asc) }
+ scope :order_by_created_at_desc, -> { reorder(created_at: :desc) }
+ # After we denormalize the `released_at` column, we won't need to use `joins(:release)` and keyset_order_*
+ scope :order_by_released_at_asc, -> { joins(:release).keyset_order_by_released_at_asc }
+ scope :order_by_released_at_desc, -> { joins(:release).keyset_order_by_released_at_desc }
+
+ delegate :name, :description, :tag, :sha, :released_at, :author_id, to: :release
+
+ class << self
+ # In the future, we should support semantic versioning.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/427286
+ def latest
+ order_by_released_at_desc.first
+ end
+
+ # This query uses LATERAL JOIN to find the latest version for each catalog resource. To avoid
+ # joining the `catalog_resources` table, we build an in-memory table using the resource ids.
+ # Example:
+ # SELECT ...
+ # FROM (VALUES (CATALOG_RESOURCE_ID_1),(CATALOG_RESOURCE_ID_2)) catalog_resources (id)
+ # INNER JOIN LATERAL (...)
+ def latest_for_catalog_resources(catalog_resources)
+ return none if catalog_resources.empty?
+
+ catalog_resources_table = Ci::Catalog::Resource.arel_table
+ catalog_resources_id_list = catalog_resources.map { |resource| "(#{resource.id})" }.join(',')
+
+ # We need to use an alias for the `releases` table here so that it does not
+ # conflict with `joins(:release)` in the `order_by_released_at_*` scope.
+ join_query = Ci::Catalog::Resources::Version
+ .where(catalog_resources_table[:id].eq(arel_table[:catalog_resource_id]))
+ .joins("INNER JOIN releases AS rel ON rel.id = #{table_name}.release_id")
+ .order(Arel.sql('rel.released_at DESC'))
+ .limit(1)
+
+ Ci::Catalog::Resources::Version
+ .from("(VALUES #{catalog_resources_id_list}) #{catalog_resources_table.name} (id)")
+ .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{table_name} ON TRUE")
+ end
+
+ def keyset_order_by_released_at_asc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].asc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def keyset_order_by_released_at_desc
+ keyset_order = Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :released_at,
+ column_expression: Release.arel_table[:released_at],
+ order_expression: Release.arel_table[:released_at].desc,
+ nullable: :not_nullable,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: :id,
+ order_expression: Release.arel_table[:id].desc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+
+ reorder(keyset_order)
+ end
+
+ def order_by(order)
+ case order.to_s
+ when 'created_asc' then order_by_created_at_asc
+ when 'created_desc' then order_by_created_at_desc
+ when 'released_at_asc' then order_by_released_at_asc
+ else
+ order_by_released_at_desc
+ end
+ end
+ end
end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2a346f97958..fe4437a4ad6 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -306,7 +306,7 @@ module Ci
end
def expired?
- expire_at.present? && expire_at < Time.current
+ expire_at.present? && expire_at.past?
end
def expiring?
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index f389c642fd8..17809ba20d3 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -54,6 +54,11 @@ module Ci
# if the setting is disabled any project is considered to be in scope.
return true unless current_project.ci_outbound_job_token_scope_enabled?
+ if !accessed_project.private? &&
+ Feature.enabled?(:restrict_ci_job_token_for_public_and_internal_projects, accessed_project)
+ return true
+ end
+
outbound_allowlist.includes?(accessed_project)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 0a876d26cc9..cf3efc5998f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -30,9 +30,11 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }]
}.freeze
- CONFIG_EXTENSION = '.gitlab-ci.yml'
- DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
+
+ DEFAULT_CONFIG_PATH = '.gitlab-ci.yml'
+
CANCELABLE_STATUSES = (Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
+ UNLOCKABLE_STATUSES = (Ci::Pipeline.completed_statuses + [:manual]).freeze
paginates_per 15
@@ -189,6 +191,7 @@ module Ci
# this is needed to ensure tests to be covered
transition [:running] => :running
+ transition [:waiting_for_callback] => :waiting_for_callback
end
event :request_resource do
@@ -203,6 +206,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -266,6 +273,32 @@ module Ci
pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
end
+ after_transition any => UNLOCKABLE_STATUSES do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.enabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
+ # TODO: Remove this block once we've completed roll-out of ci_unlock_non_successful_pipelines
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/428408
+ after_transition any => :success do |pipeline|
+ # This is a temporary flag that we added just in case we need to totally
+ # stop unlocking pipelines due to unexpected issues during rollout.
+ next if Feature.enabled?(:ci_stop_unlock_pipelines, pipeline.project)
+
+ next unless Feature.disabled?(:ci_unlock_non_successful_pipelines, pipeline.project)
+
+ pipeline.run_after_commit do
+ Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(pipeline.ci_ref_id)
+ end
+ end
+
after_transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success do |pipeline|
# We wait a little bit to ensure that all Ci::BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
@@ -380,7 +413,7 @@ module Ci
pipeline.run_after_commit do
next if pipeline.child?
- next unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ next unless Feature.enabled?(:widget_pipeline_pass_subscription_update, project) || project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
pipeline.all_merge_requests.opened.each do |merge_request|
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
@@ -389,6 +422,7 @@ module Ci
end
end
+ scope :with_unlockable_status, -> { with_status(*UNLOCKABLE_STATUSES) }
scope :internal, -> { where(source: internal_sources) }
scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(source: Enums::Ci::Pipeline.ci_sources.values) }
@@ -554,7 +588,7 @@ module Ci
end
def self.bridgeable_statuses
- ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource preparing pending]
+ ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource waiting_for_callback preparing pending]
end
def self.auto_devops_pipelines_completed_total
@@ -850,6 +884,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
@@ -1366,11 +1401,6 @@ module Ci
merge_request.merge_request_diff_for(merge_request_diff_sha)
end
- def reduced_build_attributes_list_for_rules?
- ::Feature.enabled?(:reduced_build_attributes_list_for_rules, project)
- end
- strong_memoize_attr :reduced_build_attributes_list_for_rules?
-
private
def add_message(severity, content)
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 8655e8eb9b8..e8ce58f2de5 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -30,15 +30,6 @@ module Ci
state :fixed, value: 3
state :broken, value: 4
state :still_failing, value: 5
-
- after_transition any => [:fixed, :success] do |ci_ref|
- # Do not try to unlock if no artifacts are locked
- next unless ci_ref.artifacts_locked?
-
- ci_ref.run_after_commit do
- Ci::Refs::UnlockPreviousPipelinesWorker.perform_async(ci_ref.id)
- end
- end
end
class << self
@@ -75,5 +66,13 @@ module Ci
self.status_name
end
end
+
+ def last_successful_ci_source_pipeline
+ pipelines.ci_sources.success.order(id: :desc).first
+ end
+
+ def last_unlockable_ci_source_pipeline
+ pipelines.ci_sources.with_unlockable_status.order(id: :desc).first
+ end
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 91c919dc662..9c30beeeb59 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -123,6 +123,8 @@ module Ci
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id })
}
+ scope :with_creator_id, -> (value) { where(creator_id: value) }
+
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
@@ -217,6 +219,8 @@ module Ci
validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type?
+ scope :with_version_prefix, ->(value) { joins(:runner_managers).merge(RunnerManager.with_version_prefix(value)) }
+
acts_as_taggable
after_destroy :cleanup_runner_queue
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index 7d8fc097f51..e6576859827 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -62,6 +62,16 @@ module Ci
scope :order_id_desc, -> { order(id: :desc) }
+ scope :with_version_prefix, ->(value) do
+ regex = version_regex_expression_for_version(value)
+ value += '.' if regex.end_with?('\.') && !value.end_with?('.')
+ substring = Arel::Nodes::NamedFunction.new('substring', [
+ Ci::RunnerManager.arel_table[:version],
+ Arel.sql("'#{regex}'::text")
+ ])
+ where(substring.eq(sanitize_sql_like(value)))
+ end
+
scope :with_upgrade_status, ->(upgrade_status) do
joins(:runner_version).where(runner_version: { status: upgrade_status })
end
@@ -137,5 +147,16 @@ module Ci
Ci::Runners::ProcessRunnerVersionUpdateWorker.perform_async(new_version)
end
+
+ def self.version_regex_expression_for_version(version)
+ case version
+ when /\d+\.\d+\.\d+/
+ '^\d+\.\d+\.\d+'
+ when /\d+\.\d+(\.)?/
+ '^\d+\.\d+\.'
+ else
+ '^\d+\.'
+ end
+ end
end
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 5b6946b04fd..475d57ee4c8 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -12,7 +12,7 @@ module Ci
:pipeline_id_convert_to_bigint, :source_pipeline_id_convert_to_bigint
], remove_with: '16.6', remove_after: '2023-10-22'
- columns_changing_default :partition_id
+ columns_changing_default :partition_id, :source_partition_id
self.table_name = "ci_sources_pipelines"
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 3a498972153..3d2df9a45ef 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -78,6 +78,10 @@ module Ci
transition any - [:running] => :running
end
+ event :wait_for_callback do
+ transition any - [:waiting_for_callback] => :waiting_for_callback
+ end
+
event :skip do
transition any - [:skipped] => :skipped
end
@@ -109,6 +113,7 @@ module Ci
when 'created' then nil
when 'waiting_for_resource' then request_resource
when 'preparing' then prepare
+ when 'waiting_for_callback' then wait_for_callback
when 'pending' then enqueue
when 'running' then run
when 'success' then succeed
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 39e12b53f21..886e6e9fbd7 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -372,9 +372,7 @@ class Commit
strong_memoize(:raw_signature_type) do
next unless @raw.instance_of?(Gitlab::Git::Commit)
- if raw_commit_from_rugged? && gpg_commit.signature_text.present?
- :PGP
- elsif defined? @raw.raw_commit.signature_type
+ if defined? @raw.raw_commit.signature_type
@raw.raw_commit.signature_type
end
end
@@ -397,10 +395,6 @@ class Commit
end
end
- def raw_commit_from_rugged?
- @raw.raw_commit.is_a?(Rugged::Commit)
- end
-
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3761aa81bf7..9f77bd8ebe2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,20 +8,24 @@ class CommitStatus < Ci::ApplicationRecord
include Presentable
include BulkInsertableAssociations
include TaggableQueries
-
- def self.switch_table_names
- if Gitlab::Utils.to_boolean(ENV['USE_CI_BUILDS_ROUTING_TABLE'])
- :p_ci_builds
- else
- :ci_builds
- end
- end
-
- self.table_name = self.switch_table_names
+ include IgnorableColumns
+
+ ignore_columns %i[
+ auto_canceled_by_id_convert_to_bigint
+ commit_id_convert_to_bigint
+ erased_by_id_convert_to_bigint
+ project_id_convert_to_bigint
+ runner_id_convert_to_bigint
+ trigger_request_id_convert_to_bigint
+ upstream_pipeline_id_convert_to_bigint
+ user_id_convert_to_bigint
+ ], remove_with: '17.0', remove_after: '2024-04-22'
+
+ self.table_name = :p_ci_builds
self.sequence_name = :ci_builds_id_seq
self.primary_key = :id
- partitionable scope: :pipeline
+ partitionable scope: :pipeline, partitioned: true
belongs_to :user
belongs_to :project
@@ -155,15 +159,15 @@ class CommitStatus < Ci::ApplicationRecord
end
event :drop do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :failed
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :failed
end
event :success do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running] => :success
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running] => :success
end
event :cancel do
- transition [:created, :waiting_for_resource, :preparing, :pending, :running, :manual, :scheduled] => :canceled
+ transition [:created, :waiting_for_resource, :preparing, :waiting_for_callback, :pending, :running, :manual, :scheduled] => :canceled
end
before_transition [:created, :waiting_for_resource, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status|
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
index 1d9cf5729cd..dfcc905b3c3 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module Analytics
module CycleAnalytics
module StageEventModel
diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb
index 1132e4e79ac..1646ed3dc7c 100644
--- a/app/models/concerns/can_move_repository_storage.rb
+++ b/app/models/concerns/can_move_repository_storage.rb
@@ -9,6 +9,9 @@ module CanMoveRepositoryStorage
# progress beforehand. Setting a repository read-only will fail if it is
# already in that state.
#
+ # It is assumed that `with_lock` is used here to ensure that no race condition
+ # appears between reading and writing the read-only column.
+ #
# @return nil. Failures will raise an exception
def set_repository_read_only!(skip_git_transfer_check: false)
with_lock do
@@ -16,10 +19,10 @@ module CanMoveRepositoryStorage
!skip_git_transfer_check && git_transfer_in_progress?
raise RepositoryReadOnlyError, _('Repository already read-only') if
- _safe_read_repository_read_only_column
+ safe_read_repository_read_only_column
raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(true)
+ update_repository_read_only_column(true)
nil
end
@@ -28,12 +31,8 @@ module CanMoveRepositoryStorage
# Set repository as writable again. Unlike setting it read-only, this will
# succeed if the repository is already writable.
def set_repository_writable!
- with_lock do
- raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
- _update_repository_read_only_column(false)
-
- nil
- end
+ raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
+ update_repository_read_only_column(false)
end
def git_transfer_in_progress?
@@ -49,13 +48,13 @@ module CanMoveRepositoryStorage
# 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
+ 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)
+ def update_repository_read_only_column(value)
update_column(:repository_read_only, value)
end
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 2971ecb04b8..fb2b12e5f00 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -6,19 +6,20 @@ module Ci
DEFAULT_STATUS = 'created'
BLOCKED_STATUS = %w[manual scheduled].freeze
- AVAILABLE_STATUSES = %w[created waiting_for_resource preparing pending running success failed canceled skipped manual scheduled].freeze
+ AVAILABLE_STATUSES = %w[created waiting_for_resource preparing waiting_for_callback pending running success failed canceled skipped manual scheduled].freeze
STARTED_STATUSES = %w[running success failed].freeze
- ACTIVE_STATUSES = %w[waiting_for_resource preparing pending running].freeze
+ ACTIVE_STATUSES = %w[waiting_for_resource preparing waiting_for_callback pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
STOPPED_STATUSES = COMPLETED_STATUSES + BLOCKED_STATUS
- ORDERED_STATUSES = %w[failed preparing pending running waiting_for_resource manual scheduled canceled success skipped created].freeze
+ ORDERED_STATUSES = %w[failed preparing pending running waiting_for_callback waiting_for_resource manual scheduled canceled success skipped created].freeze
PASSED_WITH_WARNINGS_STATUSES = %w[failed canceled].to_set.freeze
IGNORED_STATUSES = %w[manual].to_set.freeze
ALIVE_STATUSES = (ACTIVE_STATUSES + ['created']).freeze
CANCELABLE_STATUSES = (ALIVE_STATUSES + ['scheduled']).freeze
STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
failed: 4, canceled: 5, skipped: 6, manual: 7,
- scheduled: 8, preparing: 9, waiting_for_resource: 10 }.freeze
+ scheduled: 8, preparing: 9, waiting_for_resource: 10,
+ waiting_for_callback: 11 }.freeze
UnknownStatusError = Class.new(StandardError)
@@ -58,6 +59,7 @@ module Ci
state :created, value: 'created'
state :waiting_for_resource, value: 'waiting_for_resource'
state :preparing, value: 'preparing'
+ state :waiting_for_callback, value: 'waiting_for_callback'
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
@@ -72,6 +74,7 @@ module Ci
scope :waiting_for_resource, -> { with_status(:waiting_for_resource) }
scope :preparing, -> { with_status(:preparing) }
scope :relevant, -> { without_status(:created) }
+ scope :waiting_for_callback, -> { with_status(:waiting_for_callback) }
scope :running, -> { with_status(:running) }
scope :pending, -> { with_status(:pending) }
scope :success, -> { with_status(:success) }
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdf6bb31bf..201994cb321 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module CommitSignature
extend ActiveSupport::Concern
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index 2f64129b65f..e799127d69a 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module DiffPositionableNote
extend ActiveSupport::Concern
diff --git a/app/models/concerns/enums/package_metadata.rb b/app/models/concerns/enums/package_metadata.rb
index 3f107987ef6..352eb41829b 100644
--- a/app/models/concerns/enums/package_metadata.rb
+++ b/app/models/concerns/enums/package_metadata.rb
@@ -14,7 +14,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
ADVISORY_SOURCES = {
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 59aafc32d94..af8e37b4248 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -18,7 +18,8 @@ module Enums
apk: 9,
rpm: 10,
deb: 11,
- cbl_mariner: 12
+ 'cbl-mariner': 12,
+ wolfi: 13
}.with_indifferent_access.freeze
def self.component_types
diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb
index 412b1da55da..e4ee6e7e58e 100644
--- a/app/models/concerns/merge_request_reviewer_state.rb
+++ b/app/models/concerns/merge_request_reviewer_state.rb
@@ -6,7 +6,8 @@ module MergeRequestReviewerState
included do
enum state: {
unreviewed: 0,
- reviewed: 1
+ reviewed: 1,
+ requested_changes: 2
}
validates :state,
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
index 77edabb9706..b1dbebff4fb 100644
--- a/app/models/concerns/repository_storage_movable.rb
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -6,6 +6,9 @@ module RepositoryStorageMovable
included do
scope :order_created_at_desc, -> { order(created_at: :desc) }
+ scope :scheduled_or_started, -> do
+ where(state: [state_machine.states[:scheduled].value, state_machine.states[:started].value])
+ end
validates :container, presence: true
validates :state, presence: true
@@ -43,6 +46,8 @@ module RepositoryStorageMovable
transition replicated: :cleanup_failed
end
+ # An after_transition can't affect the success of the transition.
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45160#note_431071664
around_transition initial: :scheduled do |storage_move, block|
block.call
@@ -61,13 +66,9 @@ module RepositoryStorageMovable
true
end
- before_transition started: :replicated do |storage_move|
+ after_transition started: :replicated do |storage_move|
storage_move.container.set_repository_writable!
- 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.
@@ -83,6 +84,13 @@ module RepositoryStorageMovable
storage_move.container.set_repository_writable!
end
+ # This callback ensures the repository is set to writable in the event of
+ # a connection error during the :started -> :replicated transition
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/427254#note_1636072125
+ before_transition replicated: :cleanup_failed do |storage_move|
+ storage_move.container.set_repository_writable!
+ end
+
state :initial, value: 1
state :scheduled, value: 2
state :started, value: 3
@@ -93,15 +101,6 @@ module RepositoryStorageMovable
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.
- #
- # Therefore, we create this No-op method for snippets and wikis and let project
- # overwrite it in their implementation.
- def update_repository_storage(new_storage)
- # No-op
- end
-
def schedule_repository_storage_update_worker
raise NotImplementedError
end
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index 6af9ede5e8b..87b62214529 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
module RestrictedSignup
extend ActiveSupport::Concern
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index d0085b60d98..b25ee434484 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies
return false unless expirable? && token_expiration_enforced?
exp = expires_at(instance)
- !!exp && Time.current > exp
+ !!exp && exp.past?
end
def expirable?
diff --git a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
new file mode 100644
index 00000000000..c3ca3cfc038
--- /dev/null
+++ b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module UseSqlFunctionForPrimaryKeyLookups
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def find(*args)
+ return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
+ return super unless args.one?
+ return super if block_given? || primary_key.nil? || scope_attributes?
+
+ return_array = false
+ id = args.first
+
+ if id.is_a?(Array)
+ return super if id.many?
+
+ return_array = true
+
+ id = id.first
+ end
+
+ return super if id.nil? || (id.is_a?(String) && !id.number?)
+
+ from_clause = "find_#{table_name}_by_id(?) #{quoted_table_name}"
+ filter_empty_row = "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IS NOT NULL"
+ query = from(from_clause).where(filter_empty_row).limit(1).to_sql
+ # Using find_by_sql so we get query cache working
+ record = find_by_sql([query, id]).first
+
+ unless record
+ message = "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ raise(ActiveRecord::RecordNotFound.new(message, name, primary_key, id))
+ end
+
+ return_array ? [record] : record
+ end
+ end
+end
diff --git a/app/models/concerns/users/visitable.rb b/app/models/concerns/users/visitable.rb
index cb8e5fdc682..029d60d61ee 100644
--- a/app/models/concerns/users/visitable.rb
+++ b/app/models/concerns/users/visitable.rb
@@ -13,6 +13,45 @@ module Users
time = time.to_datetime
where(entity_id: entity_id, user_id: user_id, visited_at: (time - 15.minutes)..(time + 15.minutes))
end
+
+ scope :for_user, ->(user_id) { where(user_id: user_id) }
+
+ scope :recently_visited, -> do
+ where('visited_at > ?', 3.months.ago)
+ .where('visited_at <= ?', Time.current)
+ end
+
+ def self.grouped_by_week_start_and_entity_for_user(user_id:)
+ recently_visited
+ .for_user(user_id)
+ .group(:week_start, :entity_id)
+ .select(
+ :entity_id,
+ "COUNT(entity_id) AS week_count",
+ "DATE_TRUNC('week', visited_at)::date AS week_start",
+ "DENSE_RANK() OVER (ORDER BY DATE_TRUNC('week', visited_at)::date)"
+ )
+ end
+
+ def self.frecent_visits_scores(user_id:, limit:)
+ ranked_entity_visits_query = grouped_by_week_start_and_entity_for_user(user_id: user_id).to_sql
+ sql = <<~SQL
+ SELECT
+ entity_id,
+ SUM(week_count * dense_rank) AS score
+ FROM
+ (#{ranked_entity_visits_query}) as ranked_entity_visits
+ GROUP BY
+ entity_id
+ ORDER BY
+ score DESC
+ LIMIT #{limit}
+ SQL
+
+ ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
+ connection.execute(sql).to_a
+ end
+ end
end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 6a52f6a0112..15ed517dc12 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -482,6 +482,24 @@ class ContainerRepository < ApplicationRecord
raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES
end
+ def tags_page(before: nil, last: nil, sort: nil, name: nil, page_size: 100)
+ raise ArgumentError, 'not a migrated repository' unless migrated?
+
+ page = gitlab_api_client.tags(
+ self.path,
+ page_size: page_size,
+ before: before,
+ last: last,
+ sort: sort,
+ name: name
+ )
+
+ {
+ tags: transform_tags_page(page[:response_body]),
+ pagination: page[:pagination]
+ }
+ end
+
def tags_count
return 0 unless manifest && manifest['tags']
@@ -505,15 +523,11 @@ class ContainerRepository < ApplicationRecord
digests = tags.map { |tag| tag.digest }.compact.to_set
- digests.map { |digest| delete_tag_by_digest(digest) }.all?
- end
-
- def delete_tag_by_digest(digest)
- client.delete_repository_tag_by_digest(self.path, digest)
+ digests.map { |digest| delete_tag(digest) }.all?
end
- def delete_tag_by_name(name)
- client.delete_repository_tag_by_name(self.path, name)
+ def delete_tag(name_or_digest)
+ client.delete_repository_tag_by_digest(self.path, name_or_digest)
end
def start_expiration_policy!
@@ -640,6 +654,9 @@ class ContainerRepository < ApplicationRecord
tag = ContainerRegistry::Tag.new(self, raw_tag['name'])
tag.force_created_at_from_iso8601(raw_tag['created_at'])
tag.updated_at = raw_tag['updated_at']
+ tag.total_size = raw_tag['size_bytes']
+ tag.manifest_digest = raw_tag['digest']
+ tag.revision = raw_tag['config_digest'].to_s.split(':')[1]
tag
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 0bdce18bab5..f0093445ba8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -8,12 +8,15 @@ class Deployment < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include FastDestroyAll
+ include IgnorableColumns
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
ARCHIVABLE_OFFSET = 50_000
+ ignore_column :cluster_id, remove_with: '16.8', remove_after: '2023-12-21'
+
belongs_to :project, optional: false
belongs_to :environment, optional: false
belongs_to :user
diff --git a/app/models/environment.rb b/app/models/environment.rb
index efdcf7174aa..4f76fae24eb 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -8,6 +8,8 @@ class Environment < ApplicationRecord
include NullifyIfBlank
include FromUnion
+ LONG_STOP = 1.week
+
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
@@ -89,6 +91,7 @@ class Environment < ApplicationRecord
delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
+ scope :active, -> { with_state(:available, :stopping) }
scope :stopped, -> { with_state(:stopped) }
scope :order_by_last_deployed_at, -> do
@@ -104,6 +107,7 @@ class Environment < ApplicationRecord
scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
+ scope :long_stopping, -> { with_state(:stopping).where('updated_at < ?', LONG_STOP.ago) }
scope :deployed_and_updated_before, -> (project_id, before) do
# this query joins deployments and filters out any environment that has recent deployments
@@ -322,6 +326,10 @@ class Environment < ApplicationRecord
last_deployment.try(:created_at)
end
+ def long_stopping?
+ stopping? && self.updated_at < LONG_STOP.ago
+ end
+
def ref_path
"refs/#{Repository::REF_ENVIRONMENTS}/#{slug}"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c83dd24e98e..51c26767569 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -300,14 +300,15 @@ class Group < Namespace
groups.drop(1).each { |group| group.root_ancestor = root }
end
- # Returns the ids of the passed group models where the `emails_disabled`
- # column is set to true anywhere in the ancestor hierarchy.
+ # Returns the ids of the passed group models where the `emails_enabled`
+ # column is set to false anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
inner_query = inner_groups
.self_and_ancestors
- .where(emails_disabled: true)
+ .joins(:namespace_settings)
+ .where(namespace_settings: { emails_enabled: false })
.select('1')
.limit(1)
@@ -593,40 +594,13 @@ class Group < Namespace
end
def authorizable_members_with_parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.where(source_id: source_ids).select(*GroupMember.cached_column_list)
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares]).authorizable
+ Members::MembersWithParents.new(self).all_members.authorizable
end
def members_with_parents(only_active_users: true)
- # Avoids an unnecessary SELECT when the group has no parents
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_hierarchy_members = GroupMember.non_minimal_access
- .where(source_id: source_ids)
- .select(*GroupMember.cached_column_list)
-
- group_hierarchy_members = if only_active_users
- group_hierarchy_members.active_without_invites_and_requests
- else
- group_hierarchy_members.without_invites_and_requests
- end
-
- GroupMember.from_union([group_hierarchy_members,
- members_from_self_and_ancestor_group_shares])
+ Members::MembersWithParents
+ .new(self)
+ .members(active_users: only_active_users)
end
def members_from_self_and_ancestors_with_effective_access_level
@@ -671,15 +645,6 @@ class Group < Namespace
members.count
end
- # Returns all users that are members of projects
- # belonging to the current group or sub-groups
- def project_users_with_descendants
- User
- .joins(projects: :group)
- .where(namespaces: { id: self_and_descendants.select(:id) })
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
- end
-
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
@@ -996,48 +961,6 @@ class Group < Namespace
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
- def members_from_self_and_ancestor_group_shares
- group_group_link_table = GroupGroupLink.arel_table
- group_member_table = GroupMember.arel_table
-
- source_ids =
- if has_parent?
- self_and_ancestors.reorder(nil).select(:id)
- else
- id
- end
-
- group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
- cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
- cte_alias = cte.table.alias(GroupGroupLink.table_name)
-
- # Instead of members.access_level, we need to maximize that access_level at
- # the respective group_group_links.group_access.
- member_columns = GroupMember.attribute_names.map do |column_name|
- if column_name == 'access_level'
- smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
- else
- group_member_table[column_name]
- end
- end
-
- GroupMember
- .with(cte.to_arel)
- .select(*member_columns)
- .from([group_member_table, cte.alias_to(group_group_link_table)])
- .where(group_member_table[:requested_at].eq(nil))
- .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
- .where(group_member_table[:source_type].eq('Namespace'))
- .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
- .non_minimal_access
- end
-
- def smallest_value_arel(args, column_alias)
- Arel::Nodes::As.new(
- Arel::Nodes::NamedFunction.new('LEAST', args),
- Arel::Nodes::SqlLiteral.new(column_alias))
- end
-
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
deleted file mode 100644
index 9c8097e1ac8..00000000000
--- a/app/models/guest.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-class Guest
- class << self
- def can?(action, subject = :global)
- Ability.allowed?(nil, action, subject)
- end
- end
-end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index b4408301c6d..7c14c1b1716 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -237,6 +237,18 @@ class Integration < ApplicationRecord
end
private_class_method :boolean_accessor
+ def self.title
+ raise NotImplementedError
+ end
+
+ def self.description
+ raise NotImplementedError
+ end
+
+ def self.help
+ # no-op
+ end
+
def self.to_param
raise NotImplementedError
end
@@ -447,19 +459,18 @@ class Integration < ApplicationRecord
end
def title
- # implement inside child
+ self.class.title
end
def description
- # implement inside child
+ self.class.description
end
def help
- # implement inside child
+ self.class.help
end
def to_param
- # implement inside child
self.class.to_param
end
@@ -588,7 +599,7 @@ class Integration < ApplicationRecord
return if ::Gitlab::SilentMode.enabled?
return unless supported_events.include?(data[:object_kind])
- Integrations::ExecuteWorker.perform_async(id, data)
+ Integrations::ExecuteWorker.perform_async(id, data.deep_stringify_keys)
end
# override if needed
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
index ef12fc6bf6f..f8fddf8a457 100644
--- a/app/models/integrations/apple_app_store.rb
+++ b/app/models/integrations/apple_app_store.rb
@@ -37,15 +37,15 @@ module Integrations
title: -> { s_('AppleAppStore|Protected branches and tags only') },
checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
'Apple App Store Connect'
end
- def description
+ def self.description
s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.')
end
- def help
+ def self.help
variable_list = [
'<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>',
'<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>',
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 77555996cd9..39407acd6c9 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -20,15 +20,15 @@ module Integrations
title: -> { s_('Integrations|Restrict to branch (optional)') },
help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') }
- def title
+ def self.title
'Asana'
end
- def description
+ def self.description
s_('AsanaService|Add commit messages as comments to Asana tasks.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb
index 1d3616b4c3b..bbdd0e183f2 100644
--- a/app/models/integrations/assembla.rb
+++ b/app/models/integrations/assembla.rb
@@ -15,11 +15,11 @@ module Integrations
exposes_secrets: true,
placeholder: ''
- def title
+ def self.title
'Assembla'
end
- def description
+ def self.description
_('Manage projects.')
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index 9f15532a0b0..9fe73f86be3 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -38,15 +38,15 @@ module Integrations
attr_accessor :response
- def title
+ def self.title
s_('BambooService|Atlassian Bamboo')
end
- def description
+ def self.description
s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'),
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index b75801335bd..167bc210349 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -136,10 +136,6 @@ module Integrations
raise NotImplementedError
end
- def help
- raise NotImplementedError
- end
-
# With some integrations the webhook is already tied to a specific channel,
# for others the channels are configurable for each event.
def configurable_channels?
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index 09a0c9ba361..33dd9d9d387 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -36,7 +36,7 @@ module Integrations
true
end
- def help
+ def self.help
# noop
end
diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb
index 74e282f6848..3ca348e42a1 100644
--- a/app/models/integrations/bugzilla.rb
+++ b/app/models/integrations/bugzilla.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Bugzilla'
end
- def description
+ def self.description
s_("IssueTracker|Use Bugzilla as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb
index 82a5142e8c2..aab0cdf2134 100644
--- a/app/models/integrations/buildkite.rb
+++ b/app/models/integrations/buildkite.rb
@@ -75,20 +75,20 @@ module Integrations
"#{project_url}/builds?commit=#{sha}"
end
- def title
+ def self.title
'Buildkite'
end
- def description
+ def self.description
'Run CI/CD pipelines with Buildkite.'
end
- def self.to_param
- 'buildkite'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Buildkite.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Buildkite.')
+ def self.to_param
+ 'buildkite'
end
def calculate_reactive_cache(sha, ref)
diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb
index 8b5797a9d24..18268ed18f4 100644
--- a/app/models/integrations/campfire.rb
+++ b/app/models/integrations/campfire.rb
@@ -36,15 +36,15 @@ module Integrations
placeholder: '123456',
help: -> { s_('CampfireService|From the end of the room URL.') }
- def title
+ def self.title
'Campfire'
end
- def description
+ def self.description
'Send notifications about push events to Campfire chat rooms.'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'),
diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb
index 7cc05d41e14..25287b53300 100644
--- a/app/models/integrations/clickup.rb
+++ b/app/models/integrations/clickup.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/
end
- def title
+ def self.title
'ClickUp'
end
- def description
+ def self.description
s_("IssueTracker|Use Clickup as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'),
target: '_blank',
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index eda8c37fc72..f97f1fd25c9 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -22,11 +22,11 @@ module Integrations
'confluence'
end
- def title
+ def self.title
s_('ConfluenceService|Confluence Workspace')
end
- def description
+ def self.description
s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.')
end
diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb
index 3770e813eaa..fe0d01d60bd 100644
--- a/app/models/integrations/custom_issue_tracker.rb
+++ b/app/models/integrations/custom_issue_tracker.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
s_('IssueTracker|Custom issue tracker')
end
- def description
+ def self.description
s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index b1f1361afcd..5682fc2b139 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -117,15 +117,15 @@ module Integrations
# archive_trace is opt-in but we handle it with a more detailed field below
end
- def title
+ def self.title
'Datadog'
end
- def description
+ def self.description
s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
s_('DatadogIntegration|How do I set up this integration?'),
Rails.application.routes.url_helpers.help_page_url('integration/datadog'),
diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb
index 33b2b52fa62..7ce597389f0 100644
--- a/app/models/integrations/discord.rb
+++ b/app/models/integrations/discord.rb
@@ -21,23 +21,23 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("DiscordService|Discord Notifications")
end
- def description
+ def self.description
s_("DiscordService|Send notifications about project events to a Discord channel.")
end
- def self.to_param
- "discord"
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ "discord"
+ end
+
def default_channel_placeholder
s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)')
end
diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb
index f6a12c4bb1a..b59e504c98f 100644
--- a/app/models/integrations/drone_ci.rb
+++ b/app/models/integrations/drone_ci.rb
@@ -87,20 +87,20 @@ module Integrations
"gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}")
end
- def title
+ def self.title
'Drone'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def self.to_param
- 'drone_ci'
+ def self.help
+ s_('ProjectService|Run CI/CD pipelines with Drone.')
end
- def help
- s_('ProjectService|Run CI/CD pipelines with Drone.')
+ def self.to_param
+ 'drone_ci'
end
override :hook_url
diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb
index 144d1a07b04..77be8f5db45 100644
--- a/app/models/integrations/emails_on_push.rb
+++ b/app/models/integrations/emails_on_push.rb
@@ -39,11 +39,11 @@ module Integrations
recipients.split.grep(Devise.email_regexp).uniq(&:downcase)
end
- def title
+ def self.title
s_('EmailsOnPushService|Emails on push')
end
- def description
+ def self.description
s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.')
end
diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb
index 003c896704a..9d6f4c2a56c 100644
--- a/app/models/integrations/ewm.rb
+++ b/app/models/integrations/ewm.rb
@@ -10,15 +10,15 @@ module Integrations
@reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i
end
- def title
+ def self.title
'EWM'
end
- def description
+ def self.description
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb
index acacab2528e..7408f86d231 100644
--- a/app/models/integrations/external_wiki.rb
+++ b/app/models/integrations/external_wiki.rb
@@ -11,24 +11,24 @@ module Integrations
help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') },
required: true
- def title
+ def self.title
s_('ExternalWikiService|External wiki')
end
- def description
+ def self.description
s_('ExternalWikiService|Link to an external wiki from the sidebar.')
end
- def self.to_param
- 'external_wiki'
- end
-
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer'
s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
+ def self.to_param
+ 'external_wiki'
+ end
+
def sections
[
{
diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb
index 2d520eaf7e7..d008a28a226 100644
--- a/app/models/integrations/gitlab_slack_application.rb
+++ b/app/models/integrations/gitlab_slack_application.rb
@@ -26,11 +26,11 @@ module Integrations
update(active: !!slack_integration)
end
- def title
+ def self.title
s_('Integrations|GitLab for Slack app')
end
- def description
+ def self.description
s_('Integrations|Enable slash commands and notifications for a Slack workspace.')
end
diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb
index 5389e8dfa81..746f68fdc4c 100644
--- a/app/models/integrations/google_play.rb
+++ b/app/models/integrations/google_play.rb
@@ -32,15 +32,15 @@ module Integrations
title: -> { s_('GooglePlayStore|Protected branches and tags only') },
checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') }
- def title
+ def self.title
s_('GooglePlay|Google Play')
end
- def description
+ def self.description
s_('GooglePlay|Use GitLab to build and release an app in Google Play.')
end
- def help
+ def self.help
variable_list = [
'<code>SUPPLY_PACKAGE_NAME</code>',
'<code>SUPPLY_JSON_KEY_DATA</code>'
diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb
index 6e4753470a3..6a9d603e6e5 100644
--- a/app/models/integrations/hangouts_chat.rb
+++ b/app/models/integrations/hangouts_chat.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Google Chat'
end
- def description
+ def self.description
'Send notifications from GitLab to a room in Google Chat.'
end
@@ -29,7 +29,7 @@ module Integrations
'hangouts_chat'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'),
target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 559e48afd10..cc570e49e36 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -32,34 +32,32 @@ module Integrations
non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') },
required: true
- def title
+ def self.title
'Harbor'
end
- def description
+ def self.description
s_("HarborIntegration|Use Harbor as this project's container registry.")
end
- def help
+ def self.help
s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.")
end
+ def self.to_param
+ name.demodulize.downcase
+ end
+
def hostname
Gitlab::Utils.parse_url(url).hostname
end
- class << self
- def to_param
- name.demodulize.downcase
- end
-
- def supported_events
- []
- end
+ def self.supported_events
+ []
+ end
- def supported_event_actions
- []
- end
+ def self.supported_event_actions
+ []
end
def test(*_args)
diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb
index a54946f074a..a1ce0877957 100644
--- a/app/models/integrations/irker.rb
+++ b/app/models/integrations/irker.rb
@@ -53,14 +53,31 @@ module Integrations
# in the UI or API.
prop_accessor :channels
- def title
+ def self.title
s_('IrkerService|irker (IRC gateway)')
end
- def description
+ def self.description
s_('IrkerService|Send update messages to an irker server.')
end
+ def self.help
+ docs_link = ActionController::Base.helpers.link_to(
+ _('Learn more.'),
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/project/integrations/irker',
+ anchor: 'set-up-an-irker-daemon'
+ ),
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ )
+
+ format(s_(
+ 'IrkerService|Send update messages to an irker server. ' \
+ 'Before you can use this, you need to set up the irker daemon. %{docs_link}'
+ ).html_safe, docs_link: docs_link.html_safe)
+ end
+
def self.to_param
'irker'
end
@@ -85,23 +102,6 @@ module Integrations
}
end
- def help
- docs_link = ActionController::Base.helpers.link_to(
- _('Learn more.'),
- Rails.application.routes.url_helpers.help_page_url(
- 'user/project/integrations/irker',
- anchor: 'set-up-an-irker-daemon'
- ),
- target: '_blank',
- rel: 'noopener noreferrer'
- )
-
- format(s_(
- 'IrkerService|Send update messages to an irker server. ' \
- 'Before you can use this, you need to set up the irker daemon. %{docs_link}'
- ).html_safe, docs_link: docs_link.html_safe)
- end
-
private
def get_channels
diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb
index 0683c8408bc..a2f5667eaee 100644
--- a/app/models/integrations/jenkins.rb
+++ b/app/models/integrations/jenkins.rb
@@ -69,15 +69,15 @@ module Integrations
%w[push merge_request tag_push]
end
- def title
+ def self.title
'Jenkins'
end
- def description
+ def self.description
s_('Run CI/CD pipelines with Jenkins.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer'
s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index f6e99454cb1..22367ee336d 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -184,16 +184,24 @@ module Integrations
options
end
- def client
- @client ||= JIRA::Client.new(options).tap do |client|
+ def client(additional_options = {})
+ JIRA::Client.new(options.merge(additional_options)).tap do |client|
# Replaces JIRA default http client with our implementation
client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
- def help
+ def self.title
+ 'Jira'
+ end
+
+ def self.description
+ s_("JiraService|Use Jira as this project's issue tracker.")
+ end
+
+ def self.help
jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe,
- url: help_page_path('integration/jira/index'))
+ url: Gitlab::Routing.url_helpers.help_page_path('integration/jira/index'))
format(
s_("JiraService|You must configure Jira before enabling this integration. " \
"%{jira_doc_link_start}Learn more.%{link_end}"),
@@ -201,14 +209,6 @@ module Integrations
link_end: '</a>'.html_safe)
end
- def title
- 'Jira'
- end
-
- def description
- s_("JiraService|Use Jira as this project's issue tracker.")
- end
-
def self.to_param
'jira'
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index 7e391b11d82..361ff4afce8 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
_('Mattermost notifications')
end
- def description
+ def self.description
s_('Send notifications about project events to Mattermost channels.')
end
@@ -17,7 +17,7 @@ module Integrations
'mattermost'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer'
s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb
index 73cddd163e0..9554dec4168 100644
--- a/app/models/integrations/mattermost_slash_commands.rb
+++ b/app/models/integrations/mattermost_slash_commands.rb
@@ -14,11 +14,11 @@ module Integrations
false
end
- def title
+ def self.title
s_('Integrations|Mattermost slash commands')
end
- def description
+ def self.description
s_('Integrations|Perform common tasks with slash commands.')
end
diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb
index 208172d6303..3a7c848d411 100644
--- a/app/models/integrations/microsoft_teams.rb
+++ b/app/models/integrations/microsoft_teams.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Microsoft Teams notifications'
end
- def description
+ def self.description
'Send notifications about project events to Microsoft Teams.'
end
@@ -30,7 +30,7 @@ module Integrations
'microsoft_teams'
end
- def help
+ def self.help
'<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>'
end
diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb
index 2d8e26d409f..9c129ca727c 100644
--- a/app/models/integrations/mock_ci.rb
+++ b/app/models/integrations/mock_ci.rb
@@ -14,11 +14,11 @@ module Integrations
validates :mock_service_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'MockCI'
end
- def description
+ def self.description
'Mock an external CI'
end
diff --git a/app/models/integrations/mock_monitoring.rb b/app/models/integrations/mock_monitoring.rb
index 72bb292edaa..9e474078b28 100644
--- a/app/models/integrations/mock_monitoring.rb
+++ b/app/models/integrations/mock_monitoring.rb
@@ -2,11 +2,11 @@
module Integrations
class MockMonitoring < BaseMonitoring
- def title
+ def self.title
'Mock monitoring'
end
- def description
+ def self.description
'Mock monitoring service'
end
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index c0acb6c87b4..f027afe0381 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -29,11 +29,11 @@ module Integrations
validates :username, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
- def title
+ def self.title
'Packagist'
end
- def description
+ def self.description
s_('Integrations|Keep your PHP dependencies updated on Packagist.')
end
diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb
index 01efbc3e4a4..c7a93d48825 100644
--- a/app/models/integrations/pipelines_email.rb
+++ b/app/models/integrations/pipelines_email.rb
@@ -44,11 +44,11 @@ module Integrations
end
end
- def title
+ def self.title
_('Pipeline status emails')
end
- def description
+ def self.description
_('Email the pipeline status to a list of recipients.')
end
diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb
index b3cbc988dd6..97e6e3e09d1 100644
--- a/app/models/integrations/pivotaltracker.rb
+++ b/app/models/integrations/pivotaltracker.rb
@@ -20,15 +20,15 @@ module Integrations
'automatically inspect. Leave blank to include all branches.')
end
- def title
+ def self.title
'Pivotal Tracker'
end
- def description
+ def self.description
s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.')
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pivotal_tracker'), target: '_blank', rel: 'noopener noreferrer'
s_('Add commit messages as comments to Pivotal Tracker stories. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb
index ff8d07a1b4c..de923bbbdd5 100644
--- a/app/models/integrations/prometheus.rb
+++ b/app/models/integrations/prometheus.rb
@@ -51,11 +51,11 @@ module Integrations
false
end
- def title
+ def self.title
'Prometheus'
end
- def description
+ def self.description
s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards')
end
diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb
index 09e011023ed..36ff5189b0f 100644
--- a/app/models/integrations/pumble.rb
+++ b/app/models/integrations/pumble.rb
@@ -18,11 +18,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Pumble'
end
- def description
+ def self.description
s_("PumbleIntegration|Send notifications about project events to Pumble.")
end
@@ -30,7 +30,7 @@ module Integrations
'pumble'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'),
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 2feae29f627..b2c4e06e71f 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -71,11 +71,11 @@ module Integrations
]
end
- def title
+ def self.title
'Pushover'
end
- def description
+ def self.description
s_('PushoverService|Get real-time notifications on your device.')
end
diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb
index bc2a64b0848..11eda7c69f7 100644
--- a/app/models/integrations/redmine.rb
+++ b/app/models/integrations/redmine.rb
@@ -6,15 +6,15 @@ module Integrations
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
- def title
+ def self.title
'Redmine'
end
- def description
+ def self.description
s_("IssueTracker|Use Redmine as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer'
s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb
index 227fdca5c91..1d004356469 100644
--- a/app/models/integrations/shimo.rb
+++ b/app/models/integrations/shimo.rb
@@ -16,11 +16,11 @@ module Integrations
valid? && activated?
end
- def title
+ def self.title
s_('Shimo|Shimo')
end
- def description
+ def self.description
s_('Shimo|Link to a Shimo Workspace from the sidebar.')
end
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index f70376e2f0d..9f9614a84fd 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -5,11 +5,11 @@ module Integrations
include SlackMattermostNotifier
include SlackMattermostFields
- def title
+ def self.title
'Slack notifications'
end
- def description
+ def self.description
'Send notifications about project events to Slack.'
end
diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb
index b209f37ee7c..c5ea6f22951 100644
--- a/app/models/integrations/slack_slash_commands.rb
+++ b/app/models/integrations/slack_slash_commands.rb
@@ -10,11 +10,11 @@ module Integrations
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
placeholder: ''
- def title
+ def self.title
'Slack slash commands'
end
- def description
+ def self.description
"Perform common operations in Slack."
end
diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb
index bf3f391564f..1b4ab152b1d 100644
--- a/app/models/integrations/squash_tm.rb
+++ b/app/models/integrations/squash_tm.rb
@@ -22,15 +22,15 @@ module Integrations
validates :token, length: { maximum: 255 }, allow_blank: true
end
- def title
+ def self.title
'Squash TM'
end
- def description
+ def self.description
s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'),
diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb
index 575c3b8a334..913242ef9ac 100644
--- a/app/models/integrations/teamcity.rb
+++ b/app/models/integrations/teamcity.rb
@@ -47,15 +47,15 @@ module Integrations
end
end
- def title
+ def self.title
'JetBrains TeamCity'
end
- def description
+ def self.description
s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.')
end
- def help
+ def self.help
s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.')
end
diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb
index 71fe6f8d6ef..8eb1a7ad0ea 100644
--- a/app/models/integrations/telegram.rb
+++ b/app/models/integrations/telegram.rb
@@ -38,11 +38,11 @@ module Integrations
before_validation :set_webhook
- def title
+ def self.title
'Telegram'
end
- def description
+ def self.description
s_("TelegramIntegration|Send notifications about project events to Telegram.")
end
@@ -50,7 +50,7 @@ module Integrations
'telegram'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'),
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index 3b4bcfa28d3..6ee95c1173b 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
'Unify Circuit'
end
- def description
+ def self.description
s_('Integrations|Send notifications about project events to Unify Circuit.')
end
@@ -29,7 +29,7 @@ module Integrations
'unify_circuit'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb
index 3ef8ab39352..5f8cc195544 100644
--- a/app/models/integrations/webex_teams.rb
+++ b/app/models/integrations/webex_teams.rb
@@ -17,11 +17,11 @@ module Integrations
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
choices: -> { branch_choices }
- def title
+ def self.title
s_("WebexTeamsService|Webex Teams")
end
- def description
+ def self.description
s_("WebexTeamsService|Send notifications about project events to Webex Teams.")
end
@@ -29,7 +29,7 @@ module Integrations
'webex_teams'
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer'
s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb
index 15246a37aa7..932e588a829 100644
--- a/app/models/integrations/youtrack.rb
+++ b/app/models/integrations/youtrack.rb
@@ -14,15 +14,15 @@ module Integrations
@reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/
end
- def title
+ def self.title
'YouTrack'
end
- def description
+ def self.description
s_("IssueTracker|Use YouTrack as this project's issue tracker.")
end
- def help
+ def self.help
docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 58ec4abf30c..2aec0c1e871 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -57,18 +57,18 @@ module Integrations
data_fields.api_url ||= issues_tracker['api_url']
end
- def title
+ def self.title
'ZenTao'
end
- def description
+ def self.description
s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.")
end
- def help
+ def self.help
s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % {
link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- .html_safe % { url: help_page_url('user/project/integrations/zentao') },
+ .html_safe % { url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/zentao') },
link_end: '</a>'.html_safe
}
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 77e283044ea..9690e16fd7d 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -135,11 +135,12 @@ class Member < ApplicationRecord
.reorder(nil)
end
- scope :without_invites_and_requests, -> do
- active_state
- .non_request
- .non_invite
- .non_minimal_access
+ scope :without_invites_and_requests, ->(minimal_access: false) do
+ result = active_state.non_request.non_invite
+
+ result = result.non_minimal_access unless minimal_access
+
+ result
end
scope :invite, -> { where.not(invite_token: nil) }
diff --git a/app/models/members/members/members_with_parents.rb b/app/models/members/members/members_with_parents.rb
new file mode 100644
index 00000000000..61ce99e1f3e
--- /dev/null
+++ b/app/models/members/members/members_with_parents.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+module Members
+ class MembersWithParents
+ attr_reader :group
+
+ def initialize(group)
+ @group = group
+ end
+
+ # Returns all members for group and parents, with no filters
+ def all_members
+ GroupMember.from_union([
+ members_from_self_and_ancestors,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ # Returns members based on filter options:
+ #
+ # - `active_users`. DEPRECATED. If true, returns only members for active users
+ # - `minimal_access`. Used only in EE (GitLab Premium). If true, returns
+ # members which has minimal access. If false (default), does not return
+ # members with minimal access
+ #
+ # NOTE : this method does not return pending invites, nor requests.
+ def members(active_users: false, minimal_access: false)
+ raise ArgumentError, 'active_users: is deprecated' if active_users && minimal_access
+
+ group_hierarchy_members = members_from_self_and_ancestors
+
+ group_hierarchy_members =
+ if active_users
+ group_hierarchy_members.active_without_invites_and_requests
+ else
+ filter_invites_and_requests(group_hierarchy_members, minimal_access)
+ end
+
+ GroupMember.from_union([
+ group_hierarchy_members,
+ members_from_self_and_ancestor_group_shares
+ ])
+ end
+
+ private
+
+ # NOTE: minimal access is Premium, so in FOSS we will not include minimal access member
+ def filter_invites_and_requests(members, _minimal_access)
+ members.without_invites_and_requests(minimal_access: false)
+ end
+
+ def source_ids
+ # Avoids an unnecessary SELECT when the group has no parents
+ @source_ids ||=
+ if group.has_parent?
+ group.self_and_ancestors.reorder(nil).select(:id)
+ else
+ group.id
+ end
+ end
+
+ def members_from_self_and_ancestors
+ GroupMember
+ .with_source_id(source_ids)
+ .select(*GroupMember.cached_column_list)
+ end
+
+ def members_from_self_and_ancestor_group_shares
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: source_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+ cte_alias = cte.table.alias(GroupGroupLink.table_name)
+
+ # Instead of members.access_level, we need to maximize that access_level at
+ # the respective group_group_links.group_access.
+ member_columns = GroupMember.attribute_names.map do |column_name|
+ if column_name == 'access_level'
+ smallest_value_arel([cte_alias[:group_access], group_member_table[:access_level]], 'access_level')
+ else
+ group_member_table[column_name]
+ end
+ end
+
+ GroupMember
+ .with(cte.to_arel)
+ .select(*member_columns)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:requested_at].eq(nil))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .where(group_member_table[:source_type].eq('Namespace'))
+ .where(group_member_table[:state].eq(::Member::STATE_ACTIVE))
+ .non_minimal_access
+ end
+
+ def smallest_value_arel(args, column_alias)
+ Arel::Nodes::As.new(
+ Arel::Nodes::NamedFunction.new('LEAST', args),
+ Arel::Nodes::SqlLiteral.new(column_alias))
+ end
+ end
+end
+
+Members::MembersWithParents.prepend_mod
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index d07e4f9e298..5e5f9ab7385 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -48,6 +48,12 @@ class ProjectMember < Member
end
end
+ def permissible_access_level_roles_for_project_access_token(current_user, project)
+ permissible_access_level_roles(current_user, project).filter do |_, value|
+ value <= project.project_authorizations.find_by(user: current_user).access_level
+ end
+ end
+
def access_level_roles
Gitlab::Access.options
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d9726e76c4b..524a9b8074b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -384,7 +384,6 @@ class MergeRequest < ApplicationRecord
}
scope :with_csv_entity_associations, -> { preload(:assignees, :approved_by_users, :author, :milestone, metrics: [:merged_by]) }
- scope :with_jira_integration_associations, -> { preload_routables.preload(:metrics, :assignees, :author) }
scope :recently_unprepared, -> { where(prepared_at: nil).where(created_at: 2.hours.ago..).order(:created_at, :id) } # id is the tie-breaker
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
@@ -530,6 +529,14 @@ class MergeRequest < ApplicationRecord
.pluck(:target_branch)
end
+ def self.recent_source_branches(limit: 100)
+ group(:source_branch)
+ .select(:source_branch)
+ .reorder(arel_table[:updated_at].maximum.desc)
+ .limit(limit)
+ .pluck(:source_branch)
+ end
+
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'merged_at', 'merged_at_asc' then order_merged_at_asc
@@ -1235,17 +1242,14 @@ class MergeRequest < ApplicationRecord
}
end
- def mergeable?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false, check_mergeability_retry_lease: false,
- skip_draft_check: false, skip_rebase_check: false, skip_blocked_check: false)
-
- return false unless mergeable_state?(
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_draft_check: skip_draft_check,
- skip_approved_check: skip_approved_check,
- skip_blocked_check: skip_blocked_check
- )
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable?(check_mergeability_retry_lease: false, skip_rebase_check: false, **mergeable_state_check_params)
+ return false unless mergeable_state?(**mergeable_state_check_params)
check_mergeability(sync_retry_lease: check_mergeability_retry_lease)
mergeable_git_state?(skip_rebase_check: skip_rebase_check)
@@ -1275,18 +1279,16 @@ class MergeRequest < ApplicationRecord
mergeable_state_checks + mergeable_git_state_checks
end
- def mergeable_state?(
- skip_ci_check: false, skip_discussions_check: false, skip_approved_check: false,
- skip_draft_check: false, skip_blocked_check: false)
+ # mergeable_state_check_params allows a hash of merge checks to skip or not
+ # skip_ci_check
+ # skip_discussions_check
+ # skip_draft_check
+ # skip_approved_check
+ # skip_blocked_check
+ def mergeable_state?(**mergeable_state_check_params)
additional_checks = execute_merge_checks(
self.class.mergeable_state_checks,
- params: {
- skip_ci_check: skip_ci_check,
- skip_discussions_check: skip_discussions_check,
- skip_approved_check: skip_approved_check,
- skip_draft_check: skip_draft_check,
- skip_blocked_check: skip_blocked_check
- }
+ params: mergeable_state_check_params
)
additional_checks.success?
end
@@ -1386,7 +1388,7 @@ class MergeRequest < ApplicationRecord
end
def mergeable_discussions_state?
- return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_all_discussions_are_resolved?
unresolved_notes.none?(&:to_be_resolved?)
end
@@ -1566,8 +1568,16 @@ class MergeRequest < ApplicationRecord
access.can_push_to_branch?(target_branch)
end
+ def only_allow_merge_if_pipeline_succeeds?
+ project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ end
+
+ def only_allow_merge_if_all_discussions_are_resolved?
+ project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
+ end
+
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
+ return true unless only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?
diff --git a/app/models/merge_request_context_commit_diff_file.rb b/app/models/merge_request_context_commit_diff_file.rb
index fdf57068928..2fb995ee512 100644
--- a/app/models/merge_request_context_commit_diff_file.rb
+++ b/app/models/merge_request_context_commit_diff_file.rb
@@ -10,7 +10,6 @@ class MergeRequestContextCommitDiffFile < ApplicationRecord
belongs_to :merge_request_context_commit, inverse_of: :diff_files
sha_attribute :sha
- alias_attribute :id, :sha
# create MergeRequestContextCommitDiffFile by given diff file record(s)
def self.bulk_insert(*args)
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index fc08dd4d9c8..790520c4123 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -6,13 +6,8 @@ class MergeRequestDiffCommit < ApplicationRecord
include BulkInsertSafe
include ShaAttribute
include CachedCommit
- include IgnorableColumns
include FromUnion
- ignore_column %i[author_name author_email committer_name committer_email],
- remove_with: '14.6',
- remove_after: '2021-11-22'
-
belongs_to :merge_request_diff
# This relation is called `commit_author` and not `author`, as the project
@@ -33,7 +28,6 @@ class MergeRequestDiffCommit < ApplicationRecord
belongs_to :committer, class_name: 'MergeRequest::DiffCommitUser'
sha_attribute :sha
- alias_attribute :id, :sha
attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
@@ -129,4 +123,8 @@ class MergeRequestDiffCommit < ApplicationRecord
def committer_email
committer&.email
end
+
+ def to_hash
+ super.merge({ 'id' => sha })
+ end
end
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index 6f4728a1d98..70eaab8c0ab 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -12,12 +12,14 @@ module Ml
validates :eid, :experiment, presence: true
validates :status, inclusion: { in: statuses.keys }
+ validates :model_version_id, uniqueness: { allow_nil: true }
belongs_to :experiment, class_name: 'Ml::Experiment'
belongs_to :user
belongs_to :package, class_name: 'Packages::Package'
belongs_to :project
belongs_to :ci_build, class_name: 'Ci::Build', optional: true
+ belongs_to :model_version, class_name: 'Ml::ModelVersion', optional: true, inverse_of: :candidate
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
has_many :metadata, class_name: 'Ml::CandidateMetadata'
diff --git a/app/models/ml/model.rb b/app/models/ml/model.rb
index 27f03ed5857..b6f7e9a0639 100644
--- a/app/models/ml/model.rb
+++ b/app/models/ml/model.rb
@@ -3,6 +3,7 @@
module Ml
class Model < ApplicationRecord
include Presentable
+ include Sortable
validates :project, :default_experiment, presence: true
validates :name,
@@ -15,15 +16,19 @@ module Ml
has_one :default_experiment, class_name: 'Ml::Experiment'
belongs_to :project
+ belongs_to :user
has_many :versions, class_name: 'Ml::ModelVersion'
+ has_many :metadata, class_name: 'Ml::ModelMetadata'
has_one :latest_version, -> { latest_by_model }, class_name: 'Ml::ModelVersion', inverse_of: :model
scope :including_latest_version, -> { includes(:latest_version) }
+ scope :including_project, -> { includes(:project) }
scope :with_version_count, -> {
left_outer_joins(:versions)
.select("ml_models.*, count(ml_model_versions.id) as version_count")
.group(:id)
}
+ scope :by_name, ->(name) { where("ml_models.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection
scope :by_project, ->(project) { where(project_id: project.id) }
def valid_default_experiment?
@@ -33,13 +38,12 @@ module Ml
errors.add(:default_experiment) unless default_experiment.project_id == project_id
end
- def self.find_or_create(project, name, experiment)
- create_with(default_experiment: experiment)
- .find_or_create_by(project: project, name: name)
- end
-
def self.by_project_id_and_id(project_id, id)
find_by(project_id: project_id, id: id)
end
+
+ def self.by_project_id_and_name(project_id, name)
+ find_by(project_id: project_id, name: name)
+ end
end
end
diff --git a/app/models/ml/model_metadata.rb b/app/models/ml/model_metadata.rb
new file mode 100644
index 00000000000..9c4273c629c
--- /dev/null
+++ b/app/models/ml/model_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ml
+ class ModelMetadata < ApplicationRecord
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :model, message: ->(metadata, _) { "'#{metadata.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :model, class_name: 'Ml::Model', optional: false
+ end
+end
diff --git a/app/models/ml/model_version.rb b/app/models/ml/model_version.rb
index e7fcde2cb5c..58da57f27d6 100644
--- a/app/models/ml/model_version.rb
+++ b/app/models/ml/model_version.rb
@@ -2,6 +2,8 @@
module Ml
class ModelVersion < ApplicationRecord
+ include Presentable
+
validates :project, :model, presence: true
validates :version,
@@ -10,11 +12,15 @@ module Ml
presence: true,
length: { maximum: 255 }
+ validates :description,
+ length: { maximum: 500 }
+
validate :valid_model?, :valid_package?
belongs_to :model, class_name: 'Ml::Model'
belongs_to :project
belongs_to :package, class_name: 'Packages::MlModel::Package', optional: true
+ has_one :candidate, class_name: 'Ml::Candidate'
delegate :name, to: :model
@@ -22,8 +28,17 @@ module Ml
scope :latest_by_model, -> { order_by_model_id_id_desc.select('DISTINCT ON (model_id) *') }
class << self
- def find_or_create!(model, version, package)
- create_with(package: package).find_or_create_by!(project: model.project, model: model, version: version)
+ def find_or_create!(model, version, package, description)
+ create_with(package: package, description: description)
+ .find_or_create_by!(project: model.project, model: model, version: version)
+ end
+
+ def by_project_id_and_id(project_id, id)
+ find_by(project_id: project_id, id: id)
+ end
+
+ def by_project_id_name_and_version(project_id, name, version)
+ joins(:model).find_by(model: { name: name, project_id: project_id }, project_id: project_id, version: version)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 733b89fcaf2..cd54ac1b24a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -18,6 +18,7 @@ class Namespace < ApplicationRecord
include Referable
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :unlock_membership_to_ldap, remove_with: '16.7', remove_after: '2023-11-16'
@@ -138,6 +139,8 @@ class Namespace < ApplicationRecord
to: :namespace_settings
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
to: :namespace_settings
+ delegate :emails_enabled, :emails_enabled=,
+ to: :namespace_settings, allow_nil: true
delegate :allow_runner_registration_token,
:allow_runner_registration_token=,
to: :namespace_settings
@@ -204,7 +207,7 @@ class Namespace < ApplicationRecord
# Make sure that the name is same as strong_memoize name in root_ancestor
# method
- attr_writer :root_ancestor, :emails_disabled_memoized
+ attr_writer :root_ancestor, :emails_enabled_memoized
class << self
def sti_class_for(type_name)
@@ -299,6 +302,14 @@ class Namespace < ApplicationRecord
super || Gitlab::CurrentSettings.default_branch_protection
end
+ def default_branch_protection_settings
+ settings = default_branch_protection_defaults
+
+ return settings unless settings.blank?
+
+ Gitlab::CurrentSettings.default_branch_protection_defaults
+ end
+
def visibility_level_field
:visibility_level
end
@@ -382,17 +393,16 @@ class Namespace < ApplicationRecord
# any ancestor can disable emails for all descendants
def emails_disabled?
- strong_memoize(:emails_disabled_memoized) do
- if parent_id
- self_and_ancestors.where(emails_disabled: true).exists?
- else
- !!emails_disabled
- end
- end
+ !emails_enabled?
end
def emails_enabled?
- !emails_disabled?
+ # If no namespace_settings, we can assume it has not changed from enabled
+ return true unless namespace_settings
+
+ strong_memoize(:emails_enabled_memoized) do
+ namespace_settings.emails_enabled?
+ end
end
def lfs_enabled?
@@ -626,8 +636,7 @@ class Namespace < ApplicationRecord
:route,
:project_setting,
:project_feature,
- pages_metadatum: :pages_deployment
- )
+ :active_pages_deployments)
end
private
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 3befcdeaec5..13d2c5a62e2 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -63,6 +63,12 @@ class NamespaceSetting < ApplicationRecord
namespace.root_ancestor.prevent_sharing_groups_outside_hierarchy
end
+ def emails_enabled?
+ return emails_enabled unless namespace.has_parent?
+
+ all_ancestors_have_emails_enabled?
+ end
+
def show_diff_preview_in_email?
return show_diff_preview_in_email unless namespace.has_parent?
@@ -89,6 +95,10 @@ class NamespaceSetting < ApplicationRecord
private
+ def all_ancestors_have_emails_enabled?
+ self.class.where(namespace_id: namespace.self_and_ancestors, emails_enabled: false).none?
+ end
+
def all_ancestors_allow_diff_preview_in_email?
!self.class.where(namespace_id: namespace.self_and_ancestors, show_diff_preview_in_email: false).exists?
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0f410d4810d..f60e7682418 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -2,7 +2,7 @@
module Network
class Graph
- attr_reader :days, :commits, :map, :notes, :repo
+ attr_reader :days, :commits, :map, :repo
def self.max_count
@max_count ||= 650
@@ -17,28 +17,10 @@ module Network
@commits = collect_commits
@days = index_commits
- @notes = collect_notes
end
protected
- def collect_notes
- return {} if Feature.enabled?(:disable_network_graph_notes_count, @project, type: :experiment)
-
- h = Hash.new(0)
-
- @project
- .notes
- .where(noteable_type: 'Commit')
- .group('notes.commit_id')
- .select('notes.commit_id, count(notes.id) as note_count')
- .each do |item|
- h[item.commit_id] = item.note_count.to_i
- end
-
- h
- end
-
# Get commits from repository
#
def collect_commits
diff --git a/app/models/note.rb b/app/models/note.rb
index eae7a40fb4e..6f4a56dd3cc 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -383,7 +383,11 @@ class Note < ApplicationRecord
end
def for_project_noteable?
- !(for_personal_snippet? || for_abuse_report?)
+ !(for_personal_snippet? || for_abuse_report? || group_level_issue?)
+ end
+
+ def group_level_issue?
+ (for_issue? || for_work_item?) && noteable&.project_id.blank?
end
def for_design?
diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb
index 893b08d7872..157b851e009 100644
--- a/app/models/organizations/organization.rb
+++ b/app/models/organizations/organization.rb
@@ -42,6 +42,10 @@ module Organizations
organization_users.exists?(user: user)
end
+ def web_url(only_path: nil)
+ Gitlab::UrlBuilder.build(self, only_path: only_path)
+ end
+
private
def check_if_default_organization
diff --git a/app/models/packages/npm/metadata_cache.rb b/app/models/packages/npm/metadata_cache.rb
index 02efeda69cb..b6ab2a88a98 100644
--- a/app/models/packages/npm/metadata_cache.rb
+++ b/app/models/packages/npm/metadata_cache.rb
@@ -5,6 +5,9 @@ module Packages
class MetadataCache < ApplicationRecord
include FileStoreMounter
include Packages::Downloadable
+ include Packages::Destructible
+
+ enum status: { default: 0, processing: 1, error: 3 }
belongs_to :project, inverse_of: :npm_metadata_caches
@@ -18,6 +21,9 @@ module Packages
before_validation :set_object_storage_key
attr_readonly :object_storage_key
+ scope :stale, -> { where(project_id: nil) }
+ scope :pending_destruction, -> { stale.default }
+
def self.find_or_build(package_name:, project_id:)
find_or_initialize_by(
package_name: package_name,
diff --git a/app/models/packages/nuget/symbol.rb b/app/models/packages/nuget/symbol.rb
index 643b5552d84..3315f11b974 100644
--- a/app/models/packages/nuget/symbol.rb
+++ b/app/models/packages/nuget/symbol.rb
@@ -4,6 +4,7 @@ module Packages
module Nuget
class Symbol < ApplicationRecord
include FileStoreMounter
+ include ShaAttribute
belongs_to :package, -> { where(package_type: :nuget) }, inverse_of: :nuget_symbols
@@ -13,6 +14,8 @@ module Packages
validates :signature, uniqueness: { scope: :file_path }
validates :object_storage_key, uniqueness: true
+ sha256_attribute :file_sha256
+
mount_file_store_uploader SymbolUploader
before_validation :set_object_storage_key, on: :create
diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb
index 582b51475c2..f13bcc6e32e 100644
--- a/app/models/packages/protection/rule.rb
+++ b/app/models/packages/protection/rule.rb
@@ -12,6 +12,12 @@ module Packages
validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] },
length: { maximum: 255 }
+ validates :package_name_pattern,
+ format: {
+ with: Gitlab::Regex.protection_rules_npm_package_name_pattern_regex,
+ message: ->(_object, _data) { _('should be a valid NPM package name with optional wildcard characters.') }
+ },
+ if: :npm?
validates :package_type, presence: true
validates :push_protected_up_to_access_level, presence: true
@@ -20,7 +26,7 @@ module Packages
scope :for_package_name, ->(package_name) {
return none if package_name.blank?
- where(":package_name ILIKE package_name_pattern_ilike_query", package_name: package_name)
+ where(':package_name ILIKE package_name_pattern_ilike_query', package_name: package_name)
}
def self.push_protected_from?(access_level:, package_name:, package_type:)
diff --git a/app/models/packages/pypi/metadatum.rb b/app/models/packages/pypi/metadatum.rb
index ff247fedb59..f7360409507 100644
--- a/app/models/packages/pypi/metadatum.rb
+++ b/app/models/packages/pypi/metadatum.rb
@@ -3,10 +3,24 @@
class Packages::Pypi::Metadatum < ApplicationRecord
self.primary_key = :package_id
+ MAX_REQUIRED_PYTHON_LENGTH = 255
+ MAX_KEYWORDS_LENGTH = 255
+ MAX_METADATA_VERSION_LENGTH = 16
+ MAX_AUTHOR_EMAIL_LENGTH = 2048
+ MAX_SUMMARY_LENGTH = 255
+ MAX_DESCRIPTION_LENGTH = 4000
+ MAX_DESCRIPTION_CONTENT_TYPE = 128
+
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
validates :package, presence: true
- validates :required_python, length: { maximum: 255 }, allow_nil: false
+ validates :required_python, length: { maximum: MAX_REQUIRED_PYTHON_LENGTH }, allow_nil: false
+ validates :keywords, length: { maximum: MAX_KEYWORDS_LENGTH }, allow_nil: true
+ validates :metadata_version, length: { maximum: MAX_METADATA_VERSION_LENGTH }, allow_nil: true
+ validates :author_email, length: { maximum: MAX_AUTHOR_EMAIL_LENGTH }, allow_nil: true
+ validates :summary, length: { maximum: MAX_SUMMARY_LENGTH }, allow_nil: true
+ validates :description, length: { maximum: MAX_DESCRIPTION_LENGTH }, allow_nil: true
+ validates :description_content_type, length: { maximum: MAX_DESCRIPTION_CONTENT_TYPE }, allow_nil: true
validate :pypi_package_type
diff --git a/app/models/packages/tag.rb b/app/models/packages/tag.rb
index 9c17a147bf4..0df64bfba54 100644
--- a/app/models/packages/tag.rb
+++ b/app/models/packages/tag.rb
@@ -1,9 +1,12 @@
# frozen_string_literal: true
class Packages::Tag < ApplicationRecord
belongs_to :package, inverse_of: :tags
+ belongs_to :project
validates :package, :name, presence: true
+ before_save :ensure_project_id
+
FOR_PACKAGES_TAGS_LIMIT = 200
NUGET_TAGS_SEPARATOR = ' ' # https://docs.microsoft.com/en-us/nuget/reference/nuspec#tags
@@ -15,4 +18,8 @@ class Packages::Tag < ApplicationRecord
.order(updated_at: :desc)
.limit(FOR_PACKAGES_TAGS_LIMIT)
end
+
+ def ensure_project_id
+ self.project_id ||= package.project_id
+ end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 8a02415aef4..e5e23c3bb84 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -4,8 +4,6 @@ module Pages
class LookupPath
include Gitlab::Utils::StrongMemoize
- LegacyStorageDisabledError = Class.new(::StandardError)
-
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@@ -15,6 +13,7 @@ module Pages
def project_id
project.id
end
+ strong_memoize_attr :project_id
def access_control
project.private_pages?
@@ -76,8 +75,15 @@ module Pages
attr_reader :project, :trim_prefix, :domain
+ # project.active_pages_deployments is already loaded from the database,
+ # so selecting from the array to avoid N+1
+ # this will change with when serving multiple versions on
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133261
def deployment
- project.pages_metadatum.pages_deployment
+ project
+ .active_pages_deployments
+ .to_a
+ .find { |deployment| deployment.path_prefix.blank? }
end
strong_memoize_attr :deployment
diff --git a/app/models/pages_deployment.rb b/app/models/pages_deployment.rb
index f05ed2aac6e..2aa36a94171 100644
--- a/app/models/pages_deployment.rb
+++ b/app/models/pages_deployment.rb
@@ -17,7 +17,8 @@ class PagesDeployment < ApplicationRecord
scope :with_files_stored_locally, -> { where(file_store: ::ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::ObjectStorage::Store::REMOTE) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
- scope :active, -> { where(deleted_at: nil) }
+ scope :with_path_prefix, ->(prefix) { where("COALESCE(path_prefix, '') = ?", prefix.to_s) }
+ scope :active, -> { where(deleted_at: nil).order(created_at: :desc) }
scope :deactivated, -> { where('deleted_at < ?', Time.now.utc) }
validates :file, presence: true
@@ -33,11 +34,23 @@ class PagesDeployment < ApplicationRecord
skip_callback :save, :after, :store_file!
after_commit :store_file_after_commit!, on: [:create, :update]
+ def self.latest_pipeline_id
+ Ci::Build.id_in(pluck(:ci_build_id)).maximum(:commit_id)
+ end
+
+ def self.deactivate_all(project)
+ now = Time.now.utc
+ active
+ .project_id_in(project.id)
+ .update_all(updated_at: now, deleted_at: now)
+ end
+
def self.deactivate_deployments_older_than(deployment, time: nil)
now = Time.now.utc
active
.older_than(deployment.id)
- .where(project_id: deployment.project_id, path_prefix: deployment.path_prefix)
+ .project_id_in(deployment.project_id)
+ .with_path_prefix(deployment.path_prefix)
.update_all(updated_at: now, deleted_at: time || now)
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index b86bc761cc1..cabd3924fd6 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -11,6 +11,8 @@ class PagesDomain < ApplicationRecord
MAX_CERTIFICATE_KEY_LENGTH = 8192
+ X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN = 19
+
enum certificate_source: { user_provided: 0, gitlab_provided: 1 }, _prefix: :certificate
enum scope: { instance: 0, group: 1, project: 2 }, _prefix: :scope, _default: :project
enum usage: { pages: 0, serverless: 1 }, _prefix: :usage, _default: :pages
@@ -122,15 +124,23 @@ class PagesDomain < ApplicationRecord
x509.check_private_key(pkey)
end
- def has_intermediates?
+ def has_valid_intermediates?
return false unless x509
- # self-signed certificates doesn't have the certificate chain
+ # self-signed certificates don't have the certificate chain
return true if x509.verify(x509.public_key)
store = OpenSSL::X509::Store.new
store.set_default_paths
+ store.verify_callback = ->(is_valid, store_ctx) {
+ # allow self signed certs, see https://gitlab.com/gitlab-org/gitlab/-/issues/356447
+ return true if store_ctx.error == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN
+
+ self.errors.add(:certificate, store_ctx.error_string) unless is_valid
+ is_valid
+ }
+
store.verify(x509, untrusted_ca_certs_bundle)
rescue OpenSSL::X509::StoreError
false
@@ -230,9 +240,7 @@ class PagesDomain < ApplicationRecord
end
def pages_deployed?
- return false unless project
-
- project.pages_metadatum&.deployed?
+ project&.pages_deployed?
end
private
@@ -260,9 +268,7 @@ class PagesDomain < ApplicationRecord
end
def validate_intermediates
- unless has_intermediates?
- self.errors.add(:certificate, 'misses intermediates')
- end
+ self.errors.add(:certificate, 'misses intermediates') unless has_valid_intermediates?
end
def validate_pages_domain
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 4dfe7252a0c..f2fbb5b989e 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -44,8 +44,9 @@ class PersonalAccessToken < ApplicationRecord
scope :last_used_after, -> (date) { where("last_used_at >= ?", date) }
validates :scopes, presence: true
+ validates :expires_at, presence: true, on: :create, unless: :allow_expires_at_to_be_empty?
+
validate :validate_scopes
- validates :expires_at, presence: true, on: :create
validate :expires_at_before_instance_max_expiry_date, on: :create
def revoke!
@@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord
self.class.token_prefix
end
+ def allow_expires_at_to_be_empty?
+ false
+ end
+
def expires_at_before_instance_max_expiry_date
return unless expires_at
diff --git a/app/models/project.rb b/app/models/project.rb
index fd226d23e77..0d103094aec 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -45,6 +45,7 @@ class Project < ApplicationRecord
include UpdatedAtFilterable
include IgnorableColumns
include CrossDatabaseIgnoredTables
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column :emails_disabled, remove_with: '16.3', remove_after: '2023-08-22'
@@ -140,8 +141,14 @@ class Project < ApplicationRecord
after_create -> { create_or_load_association(:pages_metadatum) }
after_create :set_timestamps_for_create
after_create :check_repository_absence!
+
+ # TODO: Remove this callback after background syncing is implemented. See https://gitlab.com/gitlab-org/gitlab/-/issues/429376.
+ after_update :update_catalog_resource,
+ if: -> { (saved_change_to_name? || saved_change_to_description? || saved_change_to_visibility_level?) && catalog_resource }
+
before_destroy :remove_private_deploy_keys
after_destroy :remove_exports
+
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
@@ -457,8 +464,10 @@ class Project < ApplicationRecord
# GitLab Pages
has_many :pages_domains
has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project
- # we need to clean up files, not only remove records
- has_many :pages_deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # rubocop:disable Cop/ActiveRecordDependent -- we need to clean up files, not only remove records
+ has_many :pages_deployments, dependent: :destroy, inverse_of: :project
+ # rubocop:enable Cop/ActiveRecordDependent
+ has_many :active_pages_deployments, -> { active }, class_name: 'PagesDeployment', inverse_of: :project
# Can be too many records. We need to implement delete_all in batches.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
@@ -497,7 +506,7 @@ class Project < ApplicationRecord
delegate :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, :monitor_access_level, :releases_access_level, :infrastructure_access_level, :model_experiments_access_level, to: :project_feature, allow_nil: true
delegate :name, to: :owner, allow_nil: true, prefix: true
- delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
+ delegate :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
delegate :last_pipeline, to: :commit, allow_nil: true
with_options to: :team do
@@ -620,42 +629,6 @@ class Project < ApplicationRecord
.or(arel_table[:storage_version].eq(nil)))
end
- scope :sorted_by_name_desc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].desc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].desc
- )
- ])
-
- reorder(keyset_order)
- }
-
- scope :sorted_by_name_asc, -> {
- keyset_order = Gitlab::Pagination::Keyset::Order.build([
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :name,
- column_expression: Project.arel_table[:name],
- order_expression: Project.arel_table[:name].asc,
- distinct: false,
- nullable: :nulls_last
- ),
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: :id,
- order_expression: Project.arel_table[:id].asc
- )
- ])
-
- reorder(keyset_order)
- }
-
scope :sorted_by_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :sorted_by_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
@@ -769,7 +742,7 @@ class Project < ApplicationRecord
end
scope :with_pages_deployed, -> do
- joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
+ where_exists(PagesDeployment.active.where('pages_deployments.project_id = projects.id'))
end
scope :pages_metadata_not_migrated, -> do
@@ -1476,12 +1449,10 @@ class Project < ApplicationRecord
end
def build_or_assign_import_data(data: nil, credentials: nil)
- return if data.nil? && credentials.nil?
-
project_import_data = import_data || build_import_data
- project_import_data.merge_data(data.to_h)
- project_import_data.merge_credentials(credentials.to_h)
+ project_import_data.merge_data(data.to_h) if data
+ project_import_data.merge_credentials(credentials.to_h) if credentials
project_import_data
end
@@ -1564,9 +1535,9 @@ class Project < ApplicationRecord
limit = creator.projects_limit
error =
if limit == 0
- _('Personal project creation is not allowed. Please contact your administrator with questions')
+ _('You cannot create projects in your personal namespace. Contact your GitLab administrator.')
else
- _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
+ _("You've reached your limit of %{limit} projects created. Contact your GitLab administrator.")
end
self.errors.add(:limit_reached, error % { limit: limit })
@@ -2236,11 +2207,11 @@ class Project < ApplicationRecord
end
def pages_deployed?
- pages_metadatum&.deployed?
+ active_pages_deployments.exists?
end
def pages_show_onboarding?
- !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
+ !(pages_metadatum&.onboarding_complete || pages_deployed?)
end
def remove_private_deploy_keys
@@ -2262,27 +2233,6 @@ class Project < ApplicationRecord
ensure_pages_metadatum.update!(onboarding_complete: true)
end
- def mark_pages_as_deployed
- ensure_pages_metadatum.update!(deployed: true)
- end
-
- def mark_pages_as_not_deployed
- ensure_pages_metadatum.update!(deployed: false)
- end
-
- def update_pages_deployment!(deployment)
- ensure_pages_metadatum.update!(pages_deployment: deployment)
- end
-
- def set_first_pages_deployment!(deployment)
- ensure_pages_metadatum
-
- # where().update_all to perform update in the single transaction with check for null
- ProjectPagesMetadatum
- .where(project_id: id, pages_deployment_id: nil)
- .update_all(deployed: deployment.present?, pages_deployment_id: deployment&.id)
- end
-
def set_full_path(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
@@ -2875,7 +2825,7 @@ class Project < ApplicationRecord
end
def uses_default_ci_config?
- ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
+ ci_config_path.blank? || Gitlab::FileDetector.type_of(ci_config_path) == :gitlab_ci
end
def limited_protected_branches(limit)
@@ -3026,7 +2976,7 @@ class Project < ApplicationRecord
end
def ci_config_for(sha)
- repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
+ repository.blob_data_at(sha, ci_config_path_or_default)
end
def enabled_group_deploy_keys
@@ -3530,6 +3480,10 @@ class Project < ApplicationRecord
pool_repository_shard == repository_storage
end
+
+ def update_catalog_resource
+ catalog_resource.sync_with_project!
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index dba81a6cb60..5e47ec6310d 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -19,19 +19,6 @@ class ProjectFeatureUsage < ApplicationRecord
end
end
- def log_jira_dvcs_integration_usage(cloud: true)
- ::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
- integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
-
- # The feature usage is used only once later to query the feature usage in a
- # long date range. Therefore, we just need to update the timestamp once per
- # day
- break if persisted? && updated_today?(integration_field)
-
- persist_jira_dvcs_usage(integration_field)
- end
- end
-
private
def updated_today?(integration_field)
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index eca2e5a740e..87cff4f2715 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -10,7 +10,4 @@ class ProjectPagesMetadatum < ApplicationRecord
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :pages_deployment
-
- scope :deployed, -> { where(deployed: true) }
- scope :with_project_route_and_deployment, -> { preload(:pages_deployment, project: [:namespace, :route]) }
end
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index ffb08e10f1f..7a80ad33d68 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -5,4 +5,6 @@ class ProjectSnippet < Snippet
validates :project, presence: true
validates :secret, inclusion: { in: [false] }
+
+ scope :by_project, ->(project) { where(project: project) }
end
diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb
index f4411e0b4fd..e2c6d1853a9 100644
--- a/app/models/projects/repository_storage_move.rb
+++ b/app/models/projects/repository_storage_move.rb
@@ -14,11 +14,6 @@ module Projects
alias_attribute :project, :container
scope :with_projects, -> { includes(container: :route) }
- override :update_repository_storage
- def update_repository_storage(new_storage)
- container.update_column(:repository_storage, new_storage)
- end
-
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
Projects::UpdateRepositoryStorageWorker.perform_async(
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index aebce59a040..40a1a4392dd 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,6 +5,7 @@ class ProtectedBranch < ApplicationRecord
include Gitlab::SQL::Pattern
include FromUnion
include EachBatch
+ include Presentable
belongs_to :group, foreign_key: :namespace_id, touch: true, inverse_of: :protected_branches
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e565de9c4ba..e639a389e0a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1102,10 +1102,6 @@ class Repository
blob_data_at(sha, '.gitlab/route-map.yml')
end
- def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml')
- blob_data_at(sha, path)
- end
-
def lfsconfig_for(sha)
blob_data_at(sha, '.lfsconfig')
end
@@ -1245,6 +1241,10 @@ class Repository
def get_patch_id(old_revision, new_revision)
raw_repository.get_patch_id(old_revision, new_revision)
rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository => e
+ # This is expected when there are no differences between the old_revision and the new_revision.
+ # It's not ideal, but is simpler to handle this here than making breaking changes to gitaly.
+ return if e.message.match?(/no difference between old and new revision./)
+
Gitlab::ErrorTracking.track_exception(
e,
project_id: project.id,
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index d5c839724d4..ad1ce740c89 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -112,7 +112,7 @@ class ResourceLabelEvent < ResourceEvent
end
def resource_parent
- issuable.project || issuable.group
+ issuable.try(:resource_parent) || issuable.project || issuable.group
end
def discussion_id_key
diff --git a/app/models/service_desk/custom_email_credential.rb b/app/models/service_desk/custom_email_credential.rb
index 5986ac8a43f..82bda673491 100644
--- a/app/models/service_desk/custom_email_credential.rb
+++ b/app/models/service_desk/custom_email_credential.rb
@@ -2,6 +2,14 @@
module ServiceDesk
class CustomEmailCredential < ApplicationRecord
+ # Used to explicitly set the SMTP AUTH method.
+ # If nil Net::SMTP will choose one of methods listed by the SMTP server.
+ enum smtp_authentication: {
+ plain: 0,
+ login: 1,
+ cram_md5: 2
+ }
+
attr_encrypted :smtp_username,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -44,7 +52,8 @@ module ServiceDesk
password: smtp_password,
address: smtp_address,
domain: Mail::Address.new(service_desk_setting.custom_email).domain,
- port: smtp_port || 587
+ port: smtp_port || 587,
+ authentication: smtp_authentication
}
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 78b0c0849e3..3e075fdaa9e 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -77,6 +77,7 @@ class Snippet < ApplicationRecord
scope :inc_relations_for_view, -> { includes(author: :status) }
scope :inc_statistics, -> { includes(:statistics) }
scope :with_statistics, -> { joins(:statistics) }
+ scope :with_repository_storage_moves, -> { joins(:repository_storage_moves) }
scope :inc_projects_namespace_route, -> { includes(project: [:route, :namespace]) }
scope :without_created_by_banned_user, -> do
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index a262802c8af..6b2fa99d547 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -31,6 +31,11 @@ class SnippetRepository < ApplicationRecord
options[:actions] = transform_file_entries(files)
+ # The Gitaly calls perform HTTP requests for permissions check
+ # Stick to the primary in order to make those requests aware that
+ # primary database must be used to fetch the data
+ self.class.sticking.stick(:user, user.id)
+
capture_git_error { repository.commit_files(user, **options) }
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 06f0115ade6..d959a6339a4 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -117,7 +117,7 @@ module System
end
def ended?
- ends_at < Time.current
+ ends_at.past?
end
def now?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index dc93decce5e..8624a1a9463 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -4,6 +4,7 @@ class SystemNoteMetadata < ApplicationRecord
include Importable
include IgnorableColumns
+ ignore_column :id_convert_to_bigint, remove_with: '16.9', remove_after: '2024-01-13'
ignore_column :note_id_convert_to_bigint, remove_with: '16.7', remove_after: '2023-11-16'
# These notes's action text might contain a reference that is external.
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 59ce9a1f37a..745a6174931 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -174,7 +174,7 @@ class Upload < ApplicationRecord
end
def update_project_statistics
- ProjectCacheWorker.perform_async(model_id, [], [:uploads_size])
+ ProjectCacheWorker.perform_async(model_id, [], ['uploads_size'])
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4034677509f..25f22563136 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -32,6 +32,7 @@ class User < MainClusterwide::ApplicationRecord
include EachBatch
include CrossDatabaseIgnoredTables
include IgnorableColumns
+ include UseSqlFunctionForPrimaryKeyLookups
ignore_column %i[
email_opted_in
@@ -48,7 +49,7 @@ class User < MainClusterwide::ApplicationRecord
# Associations with dependent: option
cross_database_ignore_tables(
- %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests],
+ %w[namespaces projects project_authorizations issues merge_requests merge_requests issues issues merge_requests events],
url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424285',
on: :destroy
)
@@ -390,6 +391,7 @@ class User < MainClusterwide::ApplicationRecord
:first_day_of_week, :first_day_of_week=,
:timezone, :timezone=,
:time_display_relative, :time_display_relative=,
+ :time_display_format, :time_display_format=,
:show_whitespace_in_diffs, :show_whitespace_in_diffs=,
:view_diffs_file_by_file, :view_diffs_file_by_file=,
:pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=,
@@ -417,6 +419,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
+ delegate :mastodon, :mastodon=, to: :user_detail, allow_nil: true
delegate :linkedin, :linkedin=, to: :user_detail, allow_nil: true
delegate :twitter, :twitter=, to: :user_detail, allow_nil: true
delegate :skype, :skype=, to: :user_detail, allow_nil: true
@@ -425,6 +428,7 @@ class User < MainClusterwide::ApplicationRecord
delegate :organization, :organization=, to: :user_detail, allow_nil: true
delegate :discord, :discord=, to: :user_detail, allow_nil: true
delegate :email_reset_offered_at, :email_reset_offered_at=, to: :user_detail, allow_nil: true
+ delegate :project_authorizations_recalculated_at, :project_authorizations_recalculated_at=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -600,6 +604,12 @@ class User < MainClusterwide::ApplicationRecord
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) }
+ scope :trusted, -> do
+ where('EXISTS (?)', ::UserCustomAttribute
+ .select(1)
+ .where('user_id = users.id')
+ .trusted_with_spam)
+ end
strip_attributes! :name
@@ -768,6 +778,8 @@ class User < MainClusterwide::ApplicationRecord
external
when 'deactivated'
deactivated
+ when "trusted"
+ trusted
else
active_without_ghosts
end
@@ -791,9 +803,9 @@ class User < MainClusterwide::ApplicationRecord
order = <<~SQL
CASE
- WHEN LOWER(users.name) = :query THEN 0
+ WHEN LOWER(users.public_email) = :query THEN 0
WHEN LOWER(users.username) = :query THEN 1
- WHEN LOWER(users.public_email) = :query THEN 2
+ WHEN LOWER(users.name) = :query THEN 2
ELSE 3
END
SQL
@@ -1081,7 +1093,7 @@ class User < MainClusterwide::ApplicationRecord
def otp_secret_expired?
return true unless otp_secret_expires_at
- otp_secret_expires_at < Time.current
+ otp_secret_expires_at.past?
end
def update_otp_secret!
@@ -1446,7 +1458,7 @@ class User < MainClusterwide::ApplicationRecord
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past?
else
false
end
@@ -2087,7 +2099,7 @@ class User < MainClusterwide::ApplicationRecord
end
def password_expired?
- !!(password_expires_at && password_expires_at < Time.current)
+ !!(password_expires_at && password_expires_at.past?)
end
def password_expired_if_applicable?
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 728c1f4844a..5a592b425df 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -20,6 +20,7 @@ class UserCustomAttribute < ApplicationRecord
TRUSTED_BY = 'trusted_by'
AUTO_BANNED_BY = 'auto_banned_by'
IDENTITY_VERIFICATION_PHONE_EXEMPT = 'identity_verification_phone_exempt'
+ IDENTITY_VERIFICATION_EXEMPT = 'identity_verification_exempt'
class << self
def upsert_custom_attributes(custom_attributes)
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 9ac814eebda..bbb08ed5774 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -17,10 +17,24 @@ class UserDetail < MainClusterwide::ApplicationRecord
DEFAULT_FIELD_LENGTH = 500
+ MASTODON_VALIDATION_REGEX = /
+ \A # beginning of string
+ @?\b # optional leading at
+ ([\w\d.%+-]+) # character group to pick up words in user portion of username
+ @ # separator between user and host
+ ( # beginning of charagter group for host portion
+ [\w\d.-]+ # character group to pick up words in host portion of username
+ \.\w{2,} # pick up tld of host domain, 2 chars or more
+ )\b # end of character group to pick up words in host portion of username
+ \z # end of string
+ /x
+
validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validate :discord_format
validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :mastodon, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validate :mastodon_format
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
@@ -32,7 +46,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
def sanitize_attrs
- %i[discord linkedin skype twitter website_url].each do |attr|
+ %i[discord linkedin mastodon skype twitter website_url].each do |attr|
value = self[attr]
self[attr] = Sanitize.clean(value) if value.present?
end
@@ -49,6 +63,7 @@ class UserDetail < MainClusterwide::ApplicationRecord
self.discord = '' if discord.nil?
self.linkedin = '' if linkedin.nil?
self.location = '' if location.nil?
+ self.mastodon = '' if mastodon.nil?
self.organization = '' if organization.nil?
self.skype = '' if skype.nil?
self.twitter = '' if twitter.nil?
@@ -62,4 +77,10 @@ def discord_format
errors.add(:discord, _('must contain only a discord user ID.'))
end
+def mastodon_format
+ return if mastodon.blank? || mastodon =~ UserDetail::MASTODON_VALIDATION_REGEX
+
+ errors.add(:mastodon, _('must contain only a mastodon username.'))
+end
+
UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 8fc9f4617d0..59cfe9a8426 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -7,6 +7,7 @@ class UserPreference < MainClusterwide::ApplicationRecord
# enum options with same name for multiple fields, also it creates
# extra methods that aren't really needed here.
NOTES_FILTERS = { all_notes: 0, only_comments: 1, only_activity: 2 }.freeze
+ TIME_DISPLAY_FORMATS = { system: 0, non_iso_format: 1, iso_format: 2 }.freeze
belongs_to :user
@@ -27,12 +28,15 @@ class UserPreference < MainClusterwide::ApplicationRecord
validates :pinned_nav_items, json_schema: { filename: 'pinned_nav_items' }
+ validates :time_display_format, inclusion: { in: TIME_DISPLAY_FORMATS.values }, presence: true
+
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
# 2023-06-22 is after 16.1 release and during 16.2 release https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#ignoring-the-column-release-m
ignore_columns :use_legacy_web_ide, remove_with: '16.2', remove_after: '2023-06-22'
attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT }
attribute :time_display_relative, default: true
+ attribute :time_display_format, default: 0
attribute :render_whitespace_in_code, default: false
attribute :project_shortcut_buttons, default: true
attribute :keyboard_shortcuts_enabled, default: true
@@ -80,6 +84,16 @@ class UserPreference < MainClusterwide::ApplicationRecord
end
end
+ class << self
+ def time_display_formats
+ {
+ s_('Time Display|System') => TIME_DISPLAY_FORMATS[:system],
+ s_('Time Display|12-hour: 2:34 PM') => TIME_DISPLAY_FORMATS[:non_iso_format],
+ s_('Time Display|24-hour: 14:34') => TIME_DISPLAY_FORMATS[:iso_format]
+ }
+ end
+ end
+
def time_display_relative
value = read_attribute(:time_display_relative)
return value unless value.nil?
diff --git a/app/models/users/anonymous.rb b/app/models/users/anonymous.rb
new file mode 100644
index 00000000000..b4a182ba203
--- /dev/null
+++ b/app/models/users/anonymous.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class Anonymous
+ class << self
+ def can?(action, subject = :global)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+ end
+end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 60dd89c3ee7..a9880e56e8c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -65,18 +65,19 @@ module Users
# 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314
# 63 and 64 were removed with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120233
branch_rules_info_callout: 65,
- create_runner_workflow_banner: 66,
+ # 66 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135470/
# 67 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121920
project_repository_limit_alert_warning_threshold: 68, # EE-only
project_repository_limit_alert_alert_threshold: 69, # EE-only
project_repository_limit_alert_error_threshold: 70, # EE-only
- new_navigation_callout: 71,
+ # 71 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134432
# 72 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129022
namespace_over_storage_users_combined_alert: 73, # EE-only
# 74 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132751
vsd_feedback_banner: 75, # EE-only
security_policy_protected_branch_modification: 76, # EE-only
- vulnerability_report_grouping: 77 # EE-only
+ vulnerability_report_grouping: 77, # EE-only
+ new_nav_for_everyone_callout: 78
}
validates :feature_name,
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 276d549006f..6d0a22c8b0a 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -2,10 +2,16 @@
module Users
class CreditCardValidation < ApplicationRecord
+ include IgnorableColumns
+
RELEASE_DAY = Date.new(2021, 5, 17)
self.table_name = 'user_credit_card_validations'
+ ignore_columns %i[last_digits network holder_name expiration_date], remove_with: '16.8', remove_after: '2023-12-22'
+
+ attr_accessor :last_digits, :network, :holder_name, :expiration_date
+
belongs_to :user
belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id,
inverse_of: :credit_card_validation
diff --git a/app/models/users/group_visit.rb b/app/models/users/group_visit.rb
index 0bcfda049fc..d7c76e2ee2c 100644
--- a/app/models/users/group_visit.rb
+++ b/app/models/users/group_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 3
+
+ def self.frecent_groups(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Group.find(ids)
+ end
end
end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index e033445d76b..2256eb8ddc4 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -41,6 +41,10 @@ module Users
).exists?
end
+ def self.by_reference_id(ref_id)
+ find_by(telesign_reference_xid: ref_id)
+ end
+
def validated?
validated_at.present?
end
diff --git a/app/models/users/project_visit.rb b/app/models/users/project_visit.rb
index 1d076e0be56..9ff3d8d2c91 100644
--- a/app/models/users/project_visit.rb
+++ b/app/models/users/project_visit.rb
@@ -13,5 +13,12 @@ module Users
validates :entity_id, presence: true
validates :user_id, presence: true
validates :visited_at, presence: true
+
+ MAX_FRECENT_ITEMS = 5
+
+ def self.frecent_projects(user_id:)
+ ids = frecent_visits_scores(user_id: user_id, limit: MAX_FRECENT_ITEMS).pluck("entity_id")
+ Project.find(ids)
+ end
end
end
diff --git a/app/models/vs_code/settings/vs_code_setting.rb b/app/models/vs_code/settings/vs_code_setting.rb
index e55d958d2b4..1401ce82045 100644
--- a/app/models/vs_code/settings/vs_code_setting.rb
+++ b/app/models/vs_code/settings/vs_code_setting.rb
@@ -5,7 +5,9 @@ module VsCode
class VsCodeSetting < ApplicationRecord
belongs_to :user, inverse_of: :vscode_settings
- validates :setting_type, presence: true
+ validates :setting_type, presence: true,
+ inclusion: { in: SETTINGS_TYPES },
+ uniqueness: { scope: :user_id }
validates :content, presence: true
scope :by_setting_type, ->(setting_type) { where(setting_type: setting_type) }
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2eed693ca76..3dd8f334a68 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -80,6 +80,7 @@ class WikiPage
alias_method :to_param, :slug
def human_title
+ return front_matter_title if Feature.enabled?(:wiki_front_matter_title, container) && front_matter_title.present?
return 'Home' if title == Wiki::HOMEPAGE
title
@@ -95,6 +96,10 @@ class WikiPage
attributes[:title] = new_title
end
+ def front_matter_title
+ front_matter[:title]
+ end
+
def raw_content
attributes[:content] ||= page&.text_data
end
@@ -320,7 +325,7 @@ class WikiPage
def serialize_front_matter(hash)
return '' unless hash.present?
- YAML.dump(hash.transform_keys(&:to_s)) + "---\n"
+ YAML.dump(hash.to_h.transform_keys(&:to_s)) + "---\n"
end
def update_front_matter(attrs)
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 0761a213532..a62d77939bf 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -73,6 +73,19 @@ class WorkItem < Issue
includes(:parent_link).order(keyset_order)
end
+ def linked_items_keyset_order
+ ::Gitlab::Pagination::Keyset::Order.build(
+ [
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'issue_link_id',
+ column_expression: IssueLink.arel_table[:id],
+ order_expression: IssueLink.arel_table[:id].asc,
+ nullable: :not_nullable,
+ distinct: true
+ )
+ ])
+ end
+
override :related_link_class
def related_link_class
WorkItems::RelatedWorkItemLink
@@ -150,7 +163,9 @@ class WorkItem < Issue
def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil)
return [] if new_record?
- linked_work_items = linked_work_items_query(link_type).preload(preload).reorder('issue_link_id')
+ linked_work_items = linked_work_items_query(link_type)
+ .preload(preload)
+ .reorder(self.class.linked_items_keyset_order)
return linked_work_items unless authorize
cross_project_filter = ->(work_items) { work_items.where(project: project) }