summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/abuse_report.rb37
-rw-r--r--app/models/achievements/achievement.rb3
-rw-r--r--app/models/achievements/user_achievement.rb17
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb8
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb5
-rw-r--r--app/models/appearance.rb8
-rw-r--r--app/models/application_setting.rb17
-rw-r--r--app/models/application_setting_implementation.rb4
-rw-r--r--app/models/bulk_imports/entity.rb4
-rw-r--r--app/models/chat_name.rb4
-rw-r--r--app/models/ci/artifact_blob.rb2
-rw-r--r--app/models/ci/bridge.rb31
-rw-r--r--app/models/ci/build.rb26
-rw-r--r--app/models/ci/build_metadata.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb2
-rw-r--r--app/models/ci/job_artifact.rb8
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/runner.rb23
-rw-r--r--app/models/ci/runner_machine.rb33
-rw-r--r--app/models/clusters/concerns/provider_status.rb2
-rw-r--r--app/models/clusters/providers/aws.rb12
-rw-r--r--app/models/clusters/providers/gcp.rb6
-rw-r--r--app/models/commit.rb14
-rw-r--r--app/models/commit_collection.rb2
-rw-r--r--app/models/commit_signatures/ssh_signature.rb7
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/concerns/analytics/cycle_analytics/parentable.rb22
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stageable.rb (renamed from app/models/concerns/analytics/cycle_analytics/stage.rb)68
-rw-r--r--app/models/concerns/board_recent_visit.rb4
-rw-r--r--app/models/concerns/ci/has_runner_executor.rb24
-rw-r--r--app/models/concerns/counter_attribute.rb40
-rw-r--r--app/models/concerns/has_user_type.rb22
-rw-r--r--app/models/concerns/noteable.rb12
-rw-r--r--app/models/concerns/project_features_compatibility.rb9
-rw-r--r--app/models/concerns/resolvable_discussion.rb2
-rw-r--r--app/models/concerns/safely_change_column_default.rb46
-rw-r--r--app/models/concerns/update_project_statistics.rb5
-rw-r--r--app/models/concerns/work_item_resource_event.rb23
-rw-r--r--app/models/deploy_key.rb8
-rw-r--r--app/models/deployment.rb11
-rw-r--r--app/models/description_version.rb2
-rw-r--r--app/models/environment.rb21
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb16
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/apple_app_store.rb111
-rw-r--r--app/models/integrations/base_chat_notification.rb16
-rw-r--r--app/models/integrations/base_slash_commands.rb2
-rw-r--r--app/models/integrations/chat_message/issue_message.rb10
-rw-r--r--app/models/integrations/chat_message/pipeline_message.rb2
-rw-r--r--app/models/integrations/field.rb7
-rw-r--r--app/models/integrations/flowdock.rb20
-rw-r--r--app/models/issue.rb7
-rw-r--r--app/models/label_note.rb15
-rw-r--r--app/models/member.rb6
-rw-r--r--app/models/members/member_role.rb8
-rw-r--r--app/models/merge_request.rb18
-rw-r--r--app/models/merge_request_diff.rb13
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/ml/candidate.rb23
-rw-r--r--app/models/namespace.rb20
-rw-r--r--app/models/namespace_setting.rb6
-rw-r--r--app/models/note.rb14
-rw-r--r--app/models/packages/nuget.rb1
-rw-r--r--app/models/packages/package.rb4
-rw-r--r--app/models/packages/package_file.rb1
-rw-r--r--app/models/pages/lookup_path.rb2
-rw-r--r--app/models/pages_domain.rb6
-rw-r--r--app/models/personal_access_token.rb20
-rw-r--r--app/models/project.rb73
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/models/project_statistics.rb29
-rw-r--r--app/models/projects/branch_rule.rb32
-rw-r--r--app/models/projects/build_artifacts_size_refresh.rb44
-rw-r--r--app/models/protected_branch.rb14
-rw-r--r--app/models/protected_branch/merge_access_level.rb1
-rw-r--r--app/models/protected_branch/push_access_level.rb1
-rw-r--r--app/models/protected_tag/create_access_level.rb1
-rw-r--r--app/models/release.rb3
-rw-r--r--app/models/repository.rb31
-rw-r--r--app/models/resource_event.rb6
-rw-r--r--app/models/resource_label_event.rb5
-rw-r--r--app/models/resource_milestone_event.rb4
-rw-r--r--app/models/resource_state_event.rb5
-rw-r--r--app/models/resource_timebox_event.rb5
-rw-r--r--app/models/synthetic_note.rb2
-rw-r--r--app/models/system_note_metadata.rb6
-rw-r--r--app/models/timelog.rb11
-rw-r--r--app/models/todo.rb14
-rw-r--r--app/models/user.rb61
-rw-r--r--app/models/user_custom_attribute.rb3
-rw-r--r--app/models/user_detail.rb31
-rw-r--r--app/models/users/namespace_commit_email.rb18
-rw-r--r--app/models/work_item.rb2
-rw-r--r--app/models/work_items/parent_link.rb10
-rw-r--r--app/models/work_items/widgets/hierarchy.rb2
96 files changed, 996 insertions, 379 deletions
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index f1f22d94061..ee0c23ef31e 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -12,14 +12,49 @@ class AbuseReport < ApplicationRecord
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
- validates :user_id, uniqueness: { message: 'has already been reported' }
+ validates :category, presence: true
+ validates :user_id,
+ uniqueness: {
+ scope: [:reporter_id, :category],
+ message: ->(object, data) do
+ _('You have already reported this user')
+ end
+ }
+
+ validates :reported_from_url,
+ allow_blank: true,
+ length: { maximum: 512 },
+ addressable_url: {
+ dns_rebind_protection: true,
+ blocked_message: 'is an invalid URL. You can try reporting the abuse again, ' \
+ 'or contact a GitLab administrator for help.'
+ }
scope :by_user, ->(user) { where(user_id: user) }
scope :with_users, -> { includes(:reporter, :user) }
+ enum category: {
+ spam: 1,
+ offensive: 2,
+ phishing: 3,
+ crypto: 4,
+ credentials: 5,
+ copyright: 6,
+ malware: 7,
+ other: 8
+ }
+
# For CacheMarkdownField
alias_method :author, :reporter
+ HUMANIZED_ATTRIBUTES = {
+ reported_from_url: "Reported from"
+ }.freeze
+
+ def self.human_attribute_name(attr, options = {})
+ HUMANIZED_ATTRIBUTES[attr.to_sym] || super
+ end
+
def remove_user(deleted_by:)
user.delete_async(deleted_by: deleted_by, params: { hard_delete: true })
end
diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb
index 904961491b5..a436e32b35b 100644
--- a/app/models/achievements/achievement.rb
+++ b/app/models/achievements/achievement.rb
@@ -7,6 +7,9 @@ module Achievements
belongs_to :namespace, inverse_of: :achievements, optional: false
+ has_many :user_achievements, inverse_of: :achievement
+ has_many :users, through: :user_achievements, inverse_of: :achievements
+
strip_attributes! :name, :description
validates :name,
diff --git a/app/models/achievements/user_achievement.rb b/app/models/achievements/user_achievement.rb
new file mode 100644
index 00000000000..885ec660cc9
--- /dev/null
+++ b/app/models/achievements/user_achievement.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Achievements
+ class UserAchievement < ApplicationRecord
+ belongs_to :achievement, inverse_of: :user_achievements, optional: false
+ belongs_to :user, inverse_of: :user_achievements, optional: false
+
+ belongs_to :awarded_by_user,
+ class_name: 'User',
+ inverse_of: :awarded_user_achievements,
+ optional: true
+ belongs_to :revoked_by_user,
+ class_name: 'User',
+ inverse_of: :revoked_user_achievements,
+ optional: true
+ end
+end
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index a888422a6b4..b432955ad88 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -2,8 +2,7 @@
class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
include FromUnion
-
- belongs_to :group, optional: false
+ include Analytics::CycleAnalytics::Parentable
validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true
@@ -58,7 +57,10 @@ class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
estimation < 1 ? nil : estimation.from_now
end
- def self.safe_create_for_group(group)
+ def self.safe_create_for_namespace(group_or_project_namespace)
+ # Namespaces::ProjectNamespace has no root_ancestor
+ # Related: https://gitlab.com/gitlab-org/gitlab/-/issues/386124
+ group = group_or_project_namespace.is_a?(Group) ? group_or_project_namespace : group_or_project_namespace.parent
top_level_group = group.root_ancestor
aggregation = find_by(group_id: top_level_group.id)
return aggregation if aggregation.present?
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index 8d3a032812e..8a80514333f 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -3,10 +3,9 @@
module Analytics
module CycleAnalytics
class ProjectStage < ApplicationRecord
- include Analytics::CycleAnalytics::Stage
+ include Analytics::CycleAnalytics::Stageable
- validates :project, presence: true
- belongs_to :project
+ belongs_to :project, optional: false
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id
alias_attribute :parent, :project
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 4a046b3ab20..3a5e06e9a1c 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -6,7 +6,7 @@ class Appearance < ApplicationRecord
include WithUploads
attribute :title, default: ''
- attribute :short_title, default: ''
+ attribute :pwa_short_name, default: ''
attribute :description, default: ''
attribute :new_project_guidelines, default: ''
attribute :profile_image_guidelines, default: ''
@@ -23,6 +23,7 @@ class Appearance < ApplicationRecord
cache_markdown_field :footer_message, pipeline: :broadcast_message
validates :logo, file_size: { maximum: 1.megabyte }
+ validates :pwa_icon, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte }
validates :message_background_color, allow_blank: true, color: true
validates :message_font_color, allow_blank: true, color: true
@@ -31,6 +32,7 @@ class Appearance < ApplicationRecord
validate :single_appearance_row, on: :create
mount_uploader :logo, AttachmentUploader
+ mount_uploader :pwa_icon, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
mount_uploader :favicon, FaviconUploader
@@ -49,6 +51,10 @@ class Appearance < ApplicationRecord
logo_system_path(logo, 'logo')
end
+ def pwa_icon_path
+ logo_system_path(pwa_icon, 'pwa_icon')
+ end
+
def header_logo_path
logo_system_path(header_logo, 'header_logo')
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3fb1f58f3e0..59ad0650eb3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -35,6 +35,7 @@ class ApplicationSetting < ApplicationRecord
belongs_to :instance_group, class_name: "Group", foreign_key: 'instance_administrators_group_id'
alias_attribute :instance_group_id, :instance_administrators_group_id
alias_attribute :instance_administrators_group, :instance_group
+ alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period
sanitizes! :default_branch_name
@@ -256,18 +257,10 @@ class ApplicationSetting < ApplicationRecord
presence: { message: 'Domain denylist cannot be empty if denylist is enabled.' },
if: :domain_denylist_enabled?
- validates :housekeeping_incremental_repack_period,
+ validates :housekeeping_optimize_repository_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
- validates :housekeeping_full_repack_period,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period }
-
- validates :housekeeping_gc_period,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period }
-
validates :terminal_max_session_time,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -413,7 +406,7 @@ class ApplicationSetting < ApplicationRecord
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :invitation_flow_enforcement, :can_create_group,
+ validates :invitation_flow_enforcement, :can_create_group, :user_defaults_to_private_profile,
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
@@ -694,6 +687,10 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :allow_runner_registration_token,
+ allow_nil: false,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 229c4e68d79..8ef7e9a92a8 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -245,7 +245,9 @@ module ApplicationSettingImplementation
users_get_by_id_limit: 300,
users_get_by_id_limit_allowlist: [],
can_create_group: true,
- bulk_import_enabled: false
+ bulk_import_enabled: false,
+ allow_runner_registration_token: true,
+ user_defaults_to_private_profile: false
}
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index e49c4e09a50..ebca5e90313 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -152,6 +152,10 @@ class BulkImports::Entity < ApplicationRecord
"::#{pluralized_name.capitalize}::UpdateService".constantize
end
+ def full_path
+ project? ? project&.full_path : group&.full_path
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index 60370c525d5..9bd618c1008 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -7,12 +7,10 @@ class ChatName < ApplicationRecord
belongs_to :user
validates :user, presence: true
- validates :integration, presence: true
validates :team_id, presence: true
validates :chat_id, presence: true
- validates :user_id, uniqueness: { scope: [:integration_id] }
- validates :chat_id, uniqueness: { scope: [:integration_id, :team_id] }
+ validates :chat_id, uniqueness: { scope: :team_id }
# Updates the "last_used_timestamp" but only if it wasn't already updated
# recently.
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index 76d4b9d6206..f87b18d516f 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -46,7 +46,7 @@ module Ci
'artifacts', path
].join('/')
- "#{project.pages_group_url}/#{artifact_path}"
+ "#{project.pages_namespace_url}/#{artifact_path}"
end
def external_link?(job)
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 662fb3cffa8..4af31fd37f2 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -19,11 +19,6 @@ module Ci
belongs_to :project
belongs_to :trigger_request
- # To be removed upon :ci_bridge_remove_sourced_pipelines feature flag removal
- has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
- foreign_key: :source_job_id,
- inverse_of: :source_bridge
-
has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
validates :ref, presence: true
@@ -89,20 +84,8 @@ module Ci
end
end
- def sourced_pipelines
- if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
- raise 'Ci::Bridge does not have sourced_pipelines association'
- end
-
- super
- end
-
def has_downstream_pipeline?
- if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
- sourced_pipeline.present?
- else
- sourced_pipelines.exists?
- end
+ sourced_pipeline.present?
end
def downstream_pipeline_params
@@ -298,7 +281,7 @@ module Ci
return [] unless forward_yaml_variables?
yaml_variables.to_a.map do |hash|
- if hash[:raw] && ci_raw_variables_in_yaml_config_enabled?
+ if hash[:raw]
{ key: hash[:key], value: hash[:value], raw: true }
else
{ key: hash[:key], value: ::ExpandVariables.expand(hash[:value], expand_variables) }
@@ -310,7 +293,7 @@ module Ci
return [] unless forward_pipeline_variables?
pipeline.variables.to_a.map do |variable|
- if variable.raw? && ci_raw_variables_in_yaml_config_enabled?
+ if variable.raw?
{ key: variable.key, value: variable.value, raw: true }
else
{ key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
@@ -323,7 +306,7 @@ module Ci
return [] unless pipeline.pipeline_schedule
pipeline.pipeline_schedule.variables.to_a.map do |variable|
- if variable.raw? && ci_raw_variables_in_yaml_config_enabled?
+ if variable.raw?
{ key: variable.key, value: variable.value, raw: true }
else
{ key: variable.key, value: ::ExpandVariables.expand(variable.value, expand_variables) }
@@ -346,12 +329,6 @@ module Ci
result.nil? ? FORWARD_DEFAULTS[:pipeline_variables] : result
end
end
-
- def ci_raw_variables_in_yaml_config_enabled?
- strong_memoize(:ci_raw_variables_in_yaml_config_enabled) do
- ::Feature.enabled?(:ci_raw_variables_in_yaml_config, project)
- end
- end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7f42b21bc87..0139b025d98 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -68,6 +68,7 @@ module Ci
delegate :service_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :harbor_integration, to: :project
+ delegate :apple_app_store_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
delegate :enable_debug_trace!, to: :metadata
@@ -587,6 +588,7 @@ module Ci
.append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
.concat(deploy_token_variables)
.concat(harbor_variables)
+ .concat(apple_app_store_variables)
end
end
@@ -630,6 +632,13 @@ module Ci
Gitlab::Ci::Variables::Collection.new(harbor_integration.ci_variables)
end
+ def apple_app_store_variables
+ return [] unless apple_app_store_integration.try(:activated?)
+ return [] unless pipeline.protected_ref?
+
+ Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables)
+ end
+
def features
{
trace_sections: true,
@@ -736,6 +745,12 @@ module Ci
self.token && token.present? && ActiveSupport::SecurityUtils.secure_compare(token, self.token)
end
+ def remove_token!
+ if Feature.enabled?(:remove_job_token_on_completion, project)
+ update!(token_encrypted: nil)
+ end
+ end
+
# acts_as_taggable uses this method create/remove tags with contexts
# defined by taggings and to get those contexts it executes a query.
# We don't use any other contexts except `tags`, so we don't need it.
@@ -884,8 +899,9 @@ module Ci
return cache unless project.ci_separated_caches
- type_suffix = pipeline.protected_ref? ? 'protected' : 'non_protected'
cache.map do |entry|
+ type_suffix = !entry[:unprotect] && pipeline.protected_ref? ? 'protected' : 'non_protected'
+
entry.merge(key: "#{entry[:key]}-#{type_suffix}")
end
end
@@ -1135,15 +1151,9 @@ module Ci
end
end
- def partition_id_token_prefix
- partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project)
- end
-
override :format_token
def format_token(token)
- return token if partition_id_token_prefix.nil?
-
- "#{partition_id_token_prefix}_#{token}"
+ "#{partition_id.to_s(16)}_#{token}"
end
protected
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 9b4794abb2e..1dcb9190f11 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -71,7 +71,7 @@ module Ci
end
def timeout_with_highest_precedence
- [(job_timeout || project_timeout), runner_timeout].compact.min_by { |timeout| timeout.value }
+ [(job_timeout || project_timeout), runner_timeout].compact.min_by(&:value)
end
def project_timeout
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 57d8b9ba368..c5f6e54c336 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -166,7 +166,7 @@ module Ci
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
self.class.with_read_consistency(build) do
- self.reset.then { |chunk| chunk.unsafe_persist_data! }
+ self.reset.then(&:unsafe_persist_data!)
end
end
rescue FailedToObtainLockError
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 53c358f4eba..0dca5b18a24 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -14,6 +14,8 @@ module Ci
include EachBatch
include Gitlab::Utils::StrongMemoize
+ enum accessibility: { public: 0, private: 1 }, _suffix: true
+
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
REPORT_FILE_TYPES = {
@@ -346,6 +348,12 @@ module Ci
end
end
+ def public_access?
+ return true unless Feature.enabled?(:non_public_artifacts, type: :development)
+
+ public_accessibility?
+ end
+
private
def store_file_in_transaction!
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 05207fb1ca0..eab2ab69e44 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -919,8 +919,12 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
next variables unless tag?
+ git_tag = project.repository.find_tag(ref)
+
+ next variables unless git_tag
+
variables.append(key: 'CI_COMMIT_TAG', value: ref)
- variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: project.repository.find_tag(ref).message)
+ variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: git_tag.message)
# legacy variable
variables.append(key: 'CI_BUILD_TAG', value: ref)
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index a7f3ff938c3..bac85b6095e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -13,6 +13,7 @@ module Ci
include TaggableQueries
include Presentable
include EachBatch
+ include Ci::HasRunnerExecutor
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration
@@ -27,21 +28,6 @@ module Ci
project_type: 3
}
- enum executor_type: {
- unknown: 0,
- custom: 1,
- shell: 2,
- docker: 3,
- docker_windows: 4,
- docker_ssh: 5,
- ssh: 6,
- parallels: 7,
- virtualbox: 8,
- docker_machine: 9,
- docker_ssh_machine: 10,
- kubernetes: 11
- }, _suffix: true
-
# This `ONLINE_CONTACT_TIMEOUT` needs to be larger than
# `RUNNER_QUEUE_EXPIRY_TIME+UPDATE_CONTACT_COLUMN_EVERY`
#
@@ -68,6 +54,7 @@ module Ci
TAG_LIST_MAX_LENGTH = 50
+ has_many :runner_machines, inverse_of: :runner
has_many :builds
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects, disable_joins: true
@@ -77,6 +64,8 @@ module Ci
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
has_one :runner_version, primary_key: :version, foreign_key: :version, class_name: 'Ci::RunnerVersion'
+ belongs_to :creator, class_name: 'User', optional: true
+
before_save :ensure_token
scope :active, -> (value = true) { where(active: value) }
@@ -440,7 +429,9 @@ module Ci
::Gitlab::Database::LoadBalancing::Session.without_sticky_writes do
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config, :executor) || {}
values[:contacted_at] = Time.current
- values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
+ if values.include?(:executor)
+ values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
+ end
cache_attributes(values)
diff --git a/app/models/ci/runner_machine.rb b/app/models/ci/runner_machine.rb
new file mode 100644
index 00000000000..1dd997a8ee1
--- /dev/null
+++ b/app/models/ci/runner_machine.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Ci
+ class RunnerMachine < Ci::ApplicationRecord
+ include FromUnion
+ include Ci::HasRunnerExecutor
+
+ belongs_to :runner
+
+ validates :runner, presence: true
+ validates :machine_xid, presence: true, length: { maximum: 64 }
+ validates :version, length: { maximum: 2048 }
+ validates :revision, length: { maximum: 255 }
+ validates :platform, length: { maximum: 255 }
+ validates :architecture, length: { maximum: 255 }
+ validates :ip_address, length: { maximum: 1024 }
+ validates :config, json_schema: { filename: 'ci_runner_config' }
+
+ # The `STALE_TIMEOUT` constant defines the how far past the last contact or creation date a runner machine
+ # will be considered stale
+ STALE_TIMEOUT = 7.days
+
+ scope :stale, -> do
+ created_some_time_ago = arel_table[:created_at].lteq(STALE_TIMEOUT.ago)
+ contacted_some_time_ago = arel_table[:contacted_at].lteq(STALE_TIMEOUT.ago)
+
+ from_union(
+ where(contacted_at: nil),
+ where(contacted_some_time_ago),
+ remove_duplicates: false).where(created_some_time_ago)
+ end
+ end
+end
diff --git a/app/models/clusters/concerns/provider_status.rb b/app/models/clusters/concerns/provider_status.rb
index 2da1ee7aabb..44da840bec3 100644
--- a/app/models/clusters/concerns/provider_status.rb
+++ b/app/models/clusters/concerns/provider_status.rb
@@ -24,7 +24,7 @@ module Clusters
transition any - [:errored] => :errored
end
- before_transition any => [:errored, :created] do |provider|
+ before_transition any => [:errored, :created] do |provider, _|
provider.nullify_credentials
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index f0f56d9ebd9..969820459e3 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -45,18 +45,6 @@ module Clusters
)
end
- def api_client
- strong_memoize(:api_client) do
- ::Aws::CloudFormation::Client.new(credentials: credentials, region: region)
- end
- end
-
- def credentials
- strong_memoize(:credentials) do
- ::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
- end
- end
-
def has_rbac_enabled?
true
end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index fde5ed592cb..6f39037b947 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -37,12 +37,6 @@ module Clusters
greater_than: 0
}
- def api_client
- return unless access_token
-
- @api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
- end
-
def nullify_credentials
assign_attributes(
access_token: nil,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5175842e5de..a95ab756600 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -359,10 +359,6 @@ class Commit
end
def has_signature?
- if signature_type == :SSH && !ssh_signatures_enabled?
- return false
- end
-
signature_type && signature_type != :NONE
end
@@ -382,10 +378,6 @@ class Commit
@signature_type ||= raw_signature_type || :NONE
end
- def ssh_signatures_enabled?
- Feature.enabled?(:ssh_commit_signatures, project)
- end
-
def signature
strong_memoize(:signature) do
case signature_type
@@ -394,7 +386,7 @@ class Commit
when :X509
Gitlab::X509::Commit.new(self).signature
when :SSH
- Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled?
+ Gitlab::Ssh::Commit.new(self).signature
else
nil
end
@@ -584,9 +576,7 @@ class Commit
private
def expire_note_etag_cache_for_related_mrs
- MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each do |mr|
- mr.expire_note_etag_cache
- end
+ MergeRequest.includes(target_project: :namespace).by_commit_sha(id).find_each(&:expire_note_etag_cache)
end
def commit_reference(from, referable_commit_id, full: false)
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index 7d89ddde0cb..47ecdfa8574 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -25,7 +25,7 @@ class CommitCollection
end
def committers
- emails = without_merge_commits.map(&:committer_email).uniq
+ emails = without_merge_commits.filter_map(&:committer_email).uniq
User.by_any_email(emails)
end
diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb
index 1e64e2b2978..e9e16651d1c 100644
--- a/app/models/commit_signatures/ssh_signature.rb
+++ b/app/models/commit_signatures/ssh_signature.rb
@@ -6,13 +6,18 @@ module CommitSignatures
include SignatureType
belongs_to :key, optional: true
+ belongs_to :user, optional: true
def type
:ssh
end
def signed_by_user
- key&.user
+ user || key&.user
+ end
+
+ def key_fingerprint_sha256
+ super || key&.fingerprint_sha256
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 2470eada62e..64e585bae14 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -71,6 +71,7 @@ class CommitStatus < Ci::ApplicationRecord
scope :scheduled_at_before, ->(date) {
where('ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?', date)
}
+ scope :with_when_executed, ->(when_executed) { where(when: when_executed) }
# The scope applies `pluck` to split the queries. Use with care.
scope :for_project_paths, -> (paths) do
diff --git a/app/models/concerns/analytics/cycle_analytics/parentable.rb b/app/models/concerns/analytics/cycle_analytics/parentable.rb
new file mode 100644
index 00000000000..785f6eea6bf
--- /dev/null
+++ b/app/models/concerns/analytics/cycle_analytics/parentable.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module Parentable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :namespace, class_name: 'Namespace', foreign_key: :group_id, optional: false # rubocop: disable Rails/InverseOf
+
+ validate :ensure_namespace_type
+
+ def ensure_namespace_type
+ return if namespace.nil?
+ return if namespace.is_a?(::Namespaces::ProjectNamespace) || namespace.is_a?(::Group)
+
+ errors.add(:namespace, s_('CycleAnalytics|the assigned object is not supported'))
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stageable.rb
index d9e6756ab86..d1f948d1366 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stageable.rb
@@ -2,7 +2,7 @@
module Analytics
module CycleAnalytics
- module Stage
+ module Stageable
extend ActiveSupport::Concern
include RelativePositioning
include Gitlab::Utils::StrongMemoize
@@ -10,7 +10,7 @@ module Analytics
included do
belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
- belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', foreign_key: :stage_event_hash_id, optional: true
+ belongs_to :stage_event_hash, class_name: 'Analytics::CycleAnalytics::StageEventHash', optional: true
validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
@@ -21,39 +21,31 @@ module Analytics
validate :validate_stage_event_pairs
validate :validate_labels
- enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier
- enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier
+ enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum,
+ _prefix: :start_event_identifier
+ enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum,
+ _prefix: :end_event_identifier
alias_attribute :custom_stage?, :custom
scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) }
scope :with_preloaded_labels, -> { includes(:start_event_label, :end_event_label) }
scope :for_list, -> { with_preloaded_labels.ordered }
- scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
+ scope :by_value_stream, ->(value_stream) { where(value_stream_id: value_stream.id) }
before_save :ensure_stage_event_hash_id
after_commit :cleanup_old_stage_event_hash
end
- def parent=(_)
- raise NotImplementedError
- end
-
- def parent
- raise NotImplementedError
- end
-
def start_event
- strong_memoize(:start_event) do
- Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
- end
+ Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
end
+ strong_memoize_attr :start_event
def end_event
- strong_memoize(:end_event) do
- Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
- end
+ Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
end
+ strong_memoize_attr :end_event
def events_hash_code
Digest::SHA256.hexdigest("#{start_event.hash_code}-#{end_event.hash_code}")
@@ -109,9 +101,9 @@ module Analytics
def validate_stage_event_pairs
return if start_event_identifier.nil? || end_event_identifier.nil?
- unless pairing_rules.fetch(start_event.class, []).include?(end_event.class)
- errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event'))
- end
+ return if pairing_rules.fetch(start_event.class, []).include?(end_event.class)
+
+ errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event'))
end
def pairing_rules
@@ -119,21 +111,23 @@ module Analytics
end
def validate_labels
- validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed?
- validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed?
+ validate_label_within_namespace(:start_event_label_id, start_event_label_id) if start_event_label_id_changed?
+ validate_label_within_namespace(:end_event_label_id, end_event_label_id) if end_event_label_id_changed?
end
- def validate_label_within_group(association_name, label_id)
+ def validate_label_within_namespace(association_name, label_id)
return unless label_id
- return unless group
- unless label_available_for_group?(label_id)
- errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group'))
- end
+ return if label_available_for_namespace?(label_id)
+
+ errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group'))
end
- def label_available_for_group?(label_id)
- LabelsFinder.new(nil, { group_id: group.id, include_ancestor_groups: true, only_group_labels: true })
+ def label_available_for_namespace?(label_id)
+ subject = is_a?(::Analytics::CycleAnalytics::Stage) ? namespace : project.group
+ return unless subject
+
+ LabelsFinder.new(nil, { group_id: subject.id, include_ancestor_groups: true, only_group_labels: true })
.execute(skip_authorization: true)
.id_in(label_id)
.exists?
@@ -142,15 +136,15 @@ module Analytics
def ensure_stage_event_hash_id
previous_stage_event_hash = stage_event_hash&.hash_sha256
- if previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash
- self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code)
- end
+ return unless previous_stage_event_hash.blank? || events_hash_code != previous_stage_event_hash
+
+ self.stage_event_hash_id = Analytics::CycleAnalytics::StageEventHash.record_id_by_hash_sha256(events_hash_code)
end
def cleanup_old_stage_event_hash
- if stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was
- Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was)
- end
+ return unless stage_event_hash_id_previously_changed? && stage_event_hash_id_previously_was
+
+ Analytics::CycleAnalytics::StageEventHash.cleanup_if_unused(stage_event_hash_id_previously_was)
end
end
end
diff --git a/app/models/concerns/board_recent_visit.rb b/app/models/concerns/board_recent_visit.rb
index fd4d574ac58..c1c8307500e 100644
--- a/app/models/concerns/board_recent_visit.rb
+++ b/app/models/concerns/board_recent_visit.rb
@@ -9,9 +9,7 @@ module BoardRecentVisit
"user" => user,
board_parent_relation => board.resource_parent,
board_relation => board
- ).tap do |visit|
- visit.touch
- end
+ ).tap(&:touch)
rescue ActiveRecord::RecordNotUnique
retry
end
diff --git a/app/models/concerns/ci/has_runner_executor.rb b/app/models/concerns/ci/has_runner_executor.rb
new file mode 100644
index 00000000000..dc70cdb2018
--- /dev/null
+++ b/app/models/concerns/ci/has_runner_executor.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Ci
+ module HasRunnerExecutor
+ extend ActiveSupport::Concern
+
+ included do
+ enum executor_type: {
+ unknown: 0,
+ custom: 1,
+ shell: 2,
+ docker: 3,
+ docker_windows: 4,
+ docker_ssh: 5,
+ ssh: 6,
+ parallels: 7,
+ virtualbox: 8,
+ docker_machine: 9,
+ docker_ssh_machine: 10,
+ kubernetes: 11
+ }, _suffix: true
+ end
+ end
+end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index f1efbba67e1..784afd1f231 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -88,12 +88,20 @@ module CounterAttribute
end
def increment_counter(attribute, increment)
- return if increment == 0
+ return if increment.amount == 0
run_after_commit_or_now do
new_value = counter(attribute).increment(increment)
- log_increment_counter(attribute, increment, new_value)
+ log_increment_counter(attribute, increment.amount, new_value)
+ end
+ end
+
+ def bulk_increment_counter(attribute, increments)
+ run_after_commit_or_now do
+ new_value = counter(attribute).bulk_increment(increments)
+
+ log_increment_counter(attribute, increments.sum(&:amount), new_value)
end
end
@@ -103,14 +111,22 @@ module CounterAttribute
end
end
- def reset_counter!(attribute)
+ def initiate_refresh!(attribute)
+ raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute)
+
detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
- counter(attribute).reset!
+ counter(attribute).initiate_refresh!
end
log_clear_counter(attribute)
end
+ def finalize_refresh(attribute)
+ raise ArgumentError, %(attribute "#{attribute}" cannot be refreshed) unless counter_attribute_enabled?(attribute)
+
+ counter(attribute).finalize_refresh
+ end
+
def execute_after_commit_callbacks
self.class.after_commit_callbacks.each do |callback|
callback.call(self.reset)
@@ -122,11 +138,17 @@ module CounterAttribute
def build_counter_for(attribute)
raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute)
- if counter_attribute_enabled?(attribute)
- Gitlab::Counters::BufferedCounter.new(self, attribute)
- else
- Gitlab::Counters::LegacyCounter.new(self, attribute)
- end
+ return legacy_counter(attribute) unless counter_attribute_enabled?(attribute)
+
+ buffered_counter(attribute)
+ end
+
+ def legacy_counter(attribute)
+ Gitlab::Counters::LegacyCounter.new(self, attribute)
+ end
+
+ def buffered_counter(attribute)
+ Gitlab::Counters::BufferedCounter.new(self, attribute)
end
def database_lock_key
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index 1af655277b8..b02c95c9662 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -14,20 +14,32 @@ module HasUserType
migration_bot: 7,
security_bot: 8,
automation_bot: 9,
- admin_bot: 11
+ admin_bot: 11,
+ suggested_reviewers_bot: 12
}.with_indifferent_access.freeze
- BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze
+ BOT_USER_TYPES = %w[
+ alert_bot
+ project_bot
+ support_bot
+ visual_review_bot
+ migration_bot
+ security_bot
+ automation_bot
+ admin_bot
+ suggested_reviewers_bot
+ ].freeze
+
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
included do
scope :humans, -> { where(user_type: :human) }
scope :bots, -> { where(user_type: BOT_USER_TYPES) }
- scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) }
+ scope :without_bots, -> { humans.or(where(user_type: USER_TYPES.keys - BOT_USER_TYPES)) }
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
- scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
- scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
+ scope :without_ghosts, -> { humans.or(where(user_type: USER_TYPES.keys - ['ghost'])) }
+ scope :without_project_bot, -> { humans.or(where(user_type: USER_TYPES.keys - ['project_bot'])) }
scope :human_or_service_user, -> { humans.or(where(user_type: :service_user)) }
enum user_type: USER_TYPES
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 492d55c74e2..eed396f785b 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -88,7 +88,7 @@ module Noteable
def discussions
@discussions ||= discussion_notes
- .inc_relations_for_view
+ .inc_relations_for_view(self)
.discussions(self)
end
@@ -126,7 +126,7 @@ module Noteable
def grouped_diff_discussions(*args)
# Doesn't use `discussion_notes`, because this may include commit diff notes
# besides MR diff notes, that we do not want to display on the MR Changes tab.
- notes.inc_relations_for_view.grouped_diff_discussions(*args)
+ notes.inc_relations_for_view(self).grouped_diff_discussions(*args)
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -205,6 +205,14 @@ module Noteable
model_name.singular
end
+ def commenters(user: nil)
+ eligable_notes = notes.user
+
+ eligable_notes = eligable_notes.not_internal unless user&.can?(:read_internal_note, self)
+
+ User.where(id: eligable_notes.select(:author_id).distinct)
+ end
+
private
# Synthetic system notes don't have discussion IDs because these are generated dynamically
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index d37f20e2e7c..b910c0ab5c2 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -124,8 +124,13 @@ module ProjectFeaturesCompatibility
private
def write_feature_attribute_boolean(field, value)
- access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
- write_feature_attribute_raw(field, access_level)
+ value_type = Gitlab::Utils.to_boolean(value)
+ if value_type.in?([true, false])
+ access_level = value_type ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+ write_feature_attribute_raw(field, access_level)
+ else
+ write_feature_attribute_string(field, value)
+ end
end
def write_feature_attribute_string(field, value)
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index 92a88d2f7c8..141c480ea1f 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -96,7 +96,7 @@ module ResolvableDiscussion
def unresolve!
return unless resolvable?
- update { |notes| notes.unresolve! }
+ update(&:unresolve!)
end
def clear_memoized_values
diff --git a/app/models/concerns/safely_change_column_default.rb b/app/models/concerns/safely_change_column_default.rb
new file mode 100644
index 00000000000..567f690d950
--- /dev/null
+++ b/app/models/concerns/safely_change_column_default.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+# == SafelyChangeColumnDefault concern.
+#
+# Contains functionality that allows safely changing a column default without downtime.
+# Without this concern, Rails can mutate the old default value to the new default value if the old default is explicitly
+# specified.
+#
+# Usage:
+#
+# class SomeModel < ApplicationRecord
+# include SafelyChangeColumnDefault
+#
+# columns_changing_default :value
+# end
+#
+# # Assume a default of 100 for value
+# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100)
+# change_column_default('some_model', 'value', from: 100, to: 101)
+# SomeModel.create!(value: 100) # INSERT INTO some_model (value) VALUES (100)
+# # Without this concern, would be INSERT INTO some_model (value) DEFAULT VALUES and would insert 101.
+module SafelyChangeColumnDefault
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Indicate that one or more columns will have their database default change.
+ #
+ # By indicating those columns here, this helper prevents a case where explicitly writing the old database default
+ # will be mutated to the new database default.
+ def columns_changing_default(*columns)
+ self.columns_with_changing_default = columns.map(&:to_s)
+ end
+ end
+
+ included do
+ class_attribute :columns_with_changing_default, default: []
+
+ before_create do
+ columns_with_changing_default.to_a.each do |attr_name|
+ attr = @attributes[attr_name]
+
+ attribute_will_change!(attr_name) if !attr.changed? && attr.came_from_user?
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 586f1dbb65c..89398537e0a 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -78,9 +78,10 @@ module UpdateProjectStatistics
return if delta == 0
return if project.nil?
+ increment = Gitlab::Counters::Increment.new(amount: delta, ref: id)
+
run_after_commit do
- ProjectStatistics.increment_statistic(
- project, self.class.project_statistics_name, delta)
+ ProjectStatistics.increment_statistic(project, self.class.project_statistics_name, increment)
end
end
end
diff --git a/app/models/concerns/work_item_resource_event.rb b/app/models/concerns/work_item_resource_event.rb
new file mode 100644
index 00000000000..d0323feb029
--- /dev/null
+++ b/app/models/concerns/work_item_resource_event.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module WorkItemResourceEvent
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :work_item, foreign_key: 'issue_id'
+ end
+
+ def work_item_synthetic_system_note(events: nil)
+ # System notes for label resource events are handled in batches, so that we have single system note for multiple
+ # label changes.
+ if is_a?(ResourceLabelEvent) && events.present?
+ return synthetic_note_class.from_events(events, resource: work_item, resource_parent: work_item.project)
+ end
+
+ synthetic_note_class.from_event(self, resource: work_item, resource_parent: work_item.project)
+ end
+
+ def synthetic_note_class
+ raise NoMethodError, 'must implement `synthetic_note_class` method'
+ end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 2563fd484f1..aaafa396337 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -19,7 +19,7 @@ class DeployKey < Key
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, namespace: :route] }) }
scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) }
- accepts_nested_attributes_for :deploy_keys_projects
+ accepts_nested_attributes_for :deploy_keys_projects, reject_if: :reject_deploy_keys_projects?
def private?
!public?
@@ -72,4 +72,10 @@ class DeployKey < Key
def impersonated?
false
end
+
+ private
+
+ def reject_deploy_keys_projects?
+ !self.valid?
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1254ce1c90a..1ae7d9925a5 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -103,15 +103,6 @@ class Deployment < ApplicationRecord
deployment.finished_at = Time.current
end
- after_transition any => :running do |deployment|
- next unless deployment.project.ci_forward_deployment_enabled?
- next if Feature.enabled?(:prevent_outdated_deployment_jobs, deployment.project)
-
- deployment.run_after_commit do
- Deployments::DropOlderDeploymentsWorker.perform_async(id)
- end
- end
-
after_transition any => :running do |deployment, transition|
deployment.run_after_commit do
Deployments::HooksWorker.perform_async(deployment_id: id, status: transition.to, status_changed_at: Time.current)
@@ -303,7 +294,7 @@ class Deployment < ApplicationRecord
end
def older_than_last_successful_deployment?
- last_deployment_id = environment.last_deployment&.id
+ last_deployment_id = environment&.last_deployment&.id
return false unless last_deployment_id.present?
return false if self.id == last_deployment_id
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index 96c8553c101..fb61b7f5fde 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -6,6 +6,8 @@ class DescriptionVersion < ApplicationRecord
validate :exactly_one_issuable
+ delegate :resource_parent, to: :issuable
+
def self.issuable_attrs
%i(issue merge_request).freeze
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index f1edfb3a34b..7d99f10822d 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -98,6 +98,27 @@ class Environment < ApplicationRecord
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 :deployed_and_updated_before, -> (project_id, before) do
+ # this query joins deployments and filters out any environment that has recent deployments
+ joins = %{
+ LEFT JOIN "deployments" on "deployments".environment_id = "environments".id
+ AND "deployments".project_id = #{project_id}
+ AND "deployments".updated_at >= #{connection.quote(before)}
+ }
+ Environment.joins(joins)
+ .where(project_id: project_id, updated_at: ...before)
+ .group('id', 'deployments.id')
+ .having('deployments.id IS NULL')
+ end
+ scope :without_protected, -> (project) {} # no-op when not in EE mode
+
+ scope :without_names, -> (names) do
+ where.not(name: names)
+ end
+ scope :without_tiers, -> (tiers) do
+ where.not(tier: tiers)
+ end
+
##
# Search environments which have names like the given query.
# Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
diff --git a/app/models/event.rb b/app/models/event.rb
index ed65b367b8a..333841b1f90 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -31,6 +31,7 @@ class Event < ApplicationRecord
DESIGN_ACTIONS = [:created, :updated, :destroyed].freeze
TEAM_ACTIONS = [:joined, :left, :expired].freeze
ISSUE_ACTIONS = [:created, :updated, :closed, :reopened].freeze
+ ISSUE_TYPES = [Issue.name, WorkItem.name].freeze
TARGET_TYPES = HashWithIndifferentAccess.new(
issue: Issue,
@@ -83,6 +84,7 @@ class Event < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') }
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
+ scope :for_issue, -> { where(target_type: ISSUE_TYPES) }
scope :for_fingerprint, ->(fingerprint) do
fingerprint.present? ? where(fingerprint: fingerprint) : none
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 0cdd7dd8596..c7ad4d61ddb 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -30,6 +30,8 @@ class Group < Namespace
has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+ has_many :namespace_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }).unscope(where: %i[source_id source_type]) },
+ foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
@@ -39,6 +41,8 @@ class Group < Namespace
source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+ has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) },
+ foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
@@ -815,7 +819,7 @@ class Group < Namespace
case state
when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override
- when SR_DISABLED_WITH_OVERRIDE then disable_shared_runners_and_allow_override!
+ when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE then disable_shared_runners_and_allow_override!
when SR_ENABLED then enable_shared_runners! # set both to true
end
end
@@ -846,7 +850,7 @@ class Group < Namespace
def has_project_with_service_desk_enabled?
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
- strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled
+ strong_memoize_attr :has_project_with_service_desk_enabled?
def activity_path
Gitlab::Routing.url_helpers.activity_group_path(self)
@@ -915,6 +919,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items_create_from_markdown)
end
+ def usage_quotas_enabled?
+ ::Feature.enabled?(:usage_quotas_for_all_editions, self) && root?
+ end
+
# Check for enabled features, similar to `Project#feature_available?`
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?
@@ -1055,7 +1063,7 @@ class Group < Namespace
end
def disable_shared_runners_and_allow_override!
- # enabled -> disabled_with_override
+ # enabled -> disabled_and_overridable
if shared_runners_enabled?
update!(
shared_runners_enabled: false,
@@ -1068,7 +1076,7 @@ class Group < Namespace
all_projects.update_all(shared_runners_enabled: false)
- # disabled_and_unoverridable -> disabled_with_override
+ # disabled_and_unoverridable -> disabled_and_overridable
else
update!(allow_descendants_override_disabled_shared_runners: true)
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index a630a6dee11..54eeab10360 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -27,7 +27,7 @@ class Integration < ApplicationRecord
# TODO Shimo is temporary disabled on group and instance-levels.
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/345677
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
- jenkins shimo
+ apple_app_store jenkins shimo
].freeze
# Fake integrations to help with local development.
@@ -75,6 +75,7 @@ class Integration < ApplicationRecord
attribute :active, default: false
attribute :alert_events, default: true
+ attribute :incident_events, default: false
attribute :category, default: 'common'
attribute :commit_events, default: true
attribute :confidential_issues_events, default: true
@@ -132,6 +133,7 @@ class Integration < ApplicationRecord
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
+ scope :incident_hooks, -> { where(incident_events: true, active: true) }
scope :deployment, -> { where(category: 'deployment') }
class << self
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
new file mode 100644
index 00000000000..84185542939
--- /dev/null
+++ b/app/models/integrations/apple_app_store.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'app_store_connect'
+
+module Integrations
+ class AppleAppStore < Integration
+ ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze
+ KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze
+
+ with_options if: :activated? do
+ validates :app_store_issuer_id, presence: true, format: { with: ISSUER_ID_REGEX }
+ validates :app_store_key_id, presence: true, format: { with: KEY_ID_REGEX }
+ validates :app_store_private_key, presence: true, certificate_key: true
+ end
+
+ field :app_store_issuer_id,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Issuer ID.') }
+
+ field :app_store_key_id,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Key ID.') },
+ is_secret: false
+
+ field :app_store_private_key,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ type: 'textarea',
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Private Key.') },
+ is_secret: false
+
+ def title
+ 'Apple App Store Connect'
+ end
+
+ def description
+ s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.')
+ end
+
+ def help
+ variable_list = [
+ '<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>',
+ '<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>',
+ '<code>APP_STORE_CONNECT_API_KEY_KEY</code>'
+ ]
+
+ # rubocop:disable Layout/LineLength
+ texts = [
+ s_("Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."),
+ s_("After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."),
+ variable_list.join('<br>'),
+ s_(format("To get started, see the <a href='%{url}' target='_blank'>integration documentation</a> for instructions on how to generate App Store Connect credentials, and how to use this integration.", url: "https://docs.gitlab.com/ee/integration/apple_app_store.html")).html_safe
+ ]
+ # rubocop:enable Layout/LineLength
+
+ texts.join('<br><br>'.html_safe)
+ end
+
+ def self.to_param
+ 'apple_app_store'
+ end
+
+ def self.supported_events
+ []
+ end
+
+ def sections
+ [
+ {
+ type: SECTION_TYPE_CONNECTION,
+ title: s_('Integrations|Integration details'),
+ description: help
+ }
+ ]
+ end
+
+ def test(*_args)
+ response = client.apps
+ if response.has_key?(:errors)
+ { success: false, message: response[:errors].first[:title] }
+ else
+ { success: true }
+ end
+ end
+
+ def ci_variables
+ return [] unless activated?
+
+ [
+ { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(app_store_private_key), masked: true,
+ public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false }
+ ]
+ end
+
+ private
+
+ def client
+ config = {
+ issuer_id: app_store_issuer_id,
+ key_id: app_store_key_id,
+ private_key: app_store_private_key
+ }
+
+ AppStoreConnect::Client.new(config)
+ end
+ end
+end
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index f2a707c2214..8700b673370 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -10,7 +10,7 @@ module Integrations
SUPPORTED_EVENTS = %w[
push issue confidential_issue merge_request note confidential_note
- tag_push pipeline wiki_page deployment
+ tag_push pipeline wiki_page deployment incident
].freeze
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[issue confidential_issue merge_request note confidential_note].freeze
@@ -76,21 +76,29 @@ module Integrations
def default_fields
[
- { type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
+ {
+ type: 'checkbox',
+ section: SECTION_TYPE_CONFIGURATION,
+ name: 'notify_only_broken_pipelines',
+ help: 'Do not send notifications for successful pipelines.'
+ }.freeze,
{
type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
choices: self.class.branch_choices
}.freeze,
{
type: 'text',
+ section: SECTION_TYPE_CONFIGURATION,
name: 'labels_to_be_notified',
placeholder: '~backend,~frontend',
help: 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
}.freeze,
{
type: 'select',
+ section: SECTION_TYPE_CONFIGURATION,
name: 'labels_to_be_notified_behavior',
choices: [
['Match any of the labels', MATCH_ANY_LABEL],
@@ -224,6 +232,7 @@ module Integrations
data.merge(project_url: project_url, project_name: project_name).with_indifferent_access
end
+ # rubocop:disable Metrics/CyclomaticComplexity
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
@@ -240,8 +249,11 @@ module Integrations
Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment"
Integrations::ChatMessage::DeploymentMessage.new(data) if notify_for_ref?(data)
+ when "incident"
+ Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
end
end
+ # rubocop:enable Metrics/CyclomaticComplexity
def build_event_channels
event_channel_names.map do |channel_field|
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index 11ff7547325..619579a543a 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -66,7 +66,7 @@ module Integrations
# rubocop: disable CodeReuse/ServiceClass
def authorize_chat_name_url(params)
- ChatNames::AuthorizeUserService.new(self, params).execute
+ ChatNames::AuthorizeUserService.new(params).execute
end
# rubocop: enable CodeReuse/ServiceClass
end
diff --git a/app/models/integrations/chat_message/issue_message.rb b/app/models/integrations/chat_message/issue_message.rb
index ca8ef670e67..1c234630370 100644
--- a/app/models/integrations/chat_message/issue_message.rb
+++ b/app/models/integrations/chat_message/issue_message.rb
@@ -9,6 +9,7 @@ module Integrations
attr_reader :action
attr_reader :state
attr_reader :description
+ attr_reader :object_kind
def initialize(params)
super
@@ -21,6 +22,7 @@ module Integrations
@action = obj_attr[:action]
@state = obj_attr[:state]
@description = obj_attr[:description] || ''
+ @object_kind = params[:object_kind]
end
def attachments
@@ -32,7 +34,7 @@ module Integrations
def activity
{
- title: "Issue #{state} by #{strip_markup(user_combined_name)}",
+ title: "#{issue_type} #{state} by #{strip_markup(user_combined_name)}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
@@ -42,7 +44,7 @@ module Integrations
private
def message
- "[#{project_link}] Issue #{issue_link} #{state} by #{strip_markup(user_combined_name)}"
+ "[#{project_link}] #{issue_type} #{issue_link} #{state} by #{strip_markup(user_combined_name)}"
end
def opened_issue?
@@ -69,6 +71,10 @@ module Integrations
def issue_title
"#{Issue.reference_prefix}#{issue_iid} #{strip_markup(title)}"
end
+
+ def issue_type
+ @issue_type ||= object_kind == 'incident' ? 'Incident' : 'Issue'
+ end
end
end
end
diff --git a/app/models/integrations/chat_message/pipeline_message.rb b/app/models/integrations/chat_message/pipeline_message.rb
index 88db40bea7f..f8a634be336 100644
--- a/app/models/integrations/chat_message/pipeline_message.rb
+++ b/app/models/integrations/chat_message/pipeline_message.rb
@@ -151,7 +151,7 @@ module Integrations
fields << failed_stages_field if failed_stages.any?
fields << failed_jobs_field if failed_jobs.any?
fields << yaml_error_field if pipeline.has_yaml_errors?
- fields << pipeline_name_field if Feature.enabled?(:pipeline_name, project) && pipeline.name.present?
+ fields << pipeline_name_field if pipeline.name.present?
fields
end
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index 53c8f5f623e..329c046075f 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -4,7 +4,7 @@ module Integrations
class Field
SECRET_NAME = %r/token|key|password|passphrase|secret/.freeze
- BOOLEAN_ATTRIBUTES = %i[required api_only exposes_secrets].freeze
+ BOOLEAN_ATTRIBUTES = %i[required api_only is_secret exposes_secrets].freeze
ATTRIBUTES = %i[
section type placeholder choices value checkbox_label
@@ -17,12 +17,13 @@ module Integrations
attr_reader :name, :integration_class
- def initialize(name:, integration_class:, type: 'text', api_only: false, **attributes)
+ def initialize(name:, integration_class:, type: 'text', is_secret: true, api_only: false, **attributes)
@name = name.to_s.freeze
@integration_class = integration_class
- attributes[:type] = SECRET_NAME.match?(@name) ? 'password' : type
+ attributes[:type] = SECRET_NAME.match?(@name) && is_secret ? 'password' : type
attributes[:api_only] = api_only
+ attributes[:is_secret] = is_secret
@attributes = attributes.freeze
invalid_attributes = attributes.keys - ATTRIBUTES
diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb
deleted file mode 100644
index d7625cfb3d2..00000000000
--- a/app/models/integrations/flowdock.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-# This integration is scheduled for removal.
-# All records must be deleted before the class can be removed.
-# https://gitlab.com/gitlab-org/gitlab/-/issues/379197
-module Integrations
- class Flowdock < Integration
- def readonly?
- true
- end
-
- def self.to_param
- 'flowdock'
- end
-
- def self.supported_events
- %w[]
- end
- end
-end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1dd11ff8315..6744ee230b0 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -46,7 +46,7 @@ class Issue < ApplicationRecord
#
# This should be kept consistent with the enums used for the GraphQL issue list query in
# https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158
- TYPES_FOR_LIST = %w(issue incident test_case task objective).freeze
+ TYPES_FOR_LIST = %w(issue incident test_case task objective key_result).freeze
# Types of issues that should be displayed on issue board lists
TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
@@ -663,11 +663,6 @@ class Issue < ApplicationRecord
author&.banned?
end
- # Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB
- def work_item_type
- super || WorkItems::Type.default_by_type(issue_type)
- end
-
def expire_etag_cache
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
Gitlab::EtagCaching::Store.new.touch(key)
diff --git a/app/models/label_note.rb b/app/models/label_note.rb
index 19dede36abd..eda650f2fa2 100644
--- a/app/models/label_note.rb
+++ b/app/models/label_note.rb
@@ -4,12 +4,19 @@ class LabelNote < SyntheticNote
attr_accessor :resource_parent
attr_reader :events
+ def self.from_event(event, resource: nil, resource_parent: nil)
+ attrs = note_attributes('label', event, resource, resource_parent).merge(events: [event])
+
+ LabelNote.new(attrs)
+ end
+
def self.from_events(events, resource: nil, resource_parent: nil)
resource ||= events.first.issuable
- attrs = note_attributes('label', events.first, resource, resource_parent).merge(events: events)
+ label_note = from_event(events.first, resource: resource, resource_parent: resource_parent)
+ label_note.events = events
- LabelNote.new(attrs)
+ label_note
end
def events=(events)
@@ -37,8 +44,8 @@ class LabelNote < SyntheticNote
end
def note_text(html: false)
- added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix)
- removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix)
+ added = labels_str(label_refs_by_action('add', html).uniq, prefix: 'added', suffix: added_suffix)
+ removed = labels_str(label_refs_by_action('remove', html).uniq, prefix: removed_prefix)
[added, removed].compact.join(' and ')
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 107530daf51..ecf9013f197 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -386,10 +386,10 @@ class Member < ApplicationRecord
user.present?
end
- def accept_request
+ def accept_request(current_user)
return false unless request?
- updated = self.update(requested_at: nil)
+ updated = self.update(requested_at: nil, created_by: current_user)
after_accept_request if updated
updated
@@ -531,7 +531,7 @@ class Member < ApplicationRecord
def send_request
notification_service.new_access_request(self)
- todo_service.create_member_access_request(self) if source_type != 'Project'
+ todo_service.create_member_access_request_todos(self)
end
def post_create_hook
diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb
index e9d7b1d3f80..36cbc97d049 100644
--- a/app/models/members/member_role.rb
+++ b/app/models/members/member_role.rb
@@ -11,6 +11,7 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
validates :base_access_level, presence: true
validate :belongs_to_top_level_namespace
validate :validate_namespace_locked, on: :update
+ validate :attributes_locked_after_member_associated, on: :update
validates_associated :members
@@ -27,4 +28,11 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
errors.add(:namespace, s_("MemberRole|can't be changed"))
end
+
+ def attributes_locked_after_member_associated
+ return unless members.present?
+
+ errors.add(:base, s_("MemberRole|cannot be changed because it is already assigned to a user. "\
+ "Please create a new Member Role instead"))
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 78c6d983a3d..0012f098ab2 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -193,6 +193,12 @@ class MergeRequest < ApplicationRecord
merge_request.merge_error = nil
end
+ before_transition any => :merged do |merge_request|
+ if ::Feature.enabled?(:reset_merge_error_on_transition, merge_request.project)
+ merge_request.merge_error = nil
+ end
+ end
+
after_transition any => :opened do |merge_request|
merge_request.run_after_commit do
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
@@ -436,6 +442,14 @@ class MergeRequest < ApplicationRecord
)
end
+ scope :without_hidden, -> {
+ if Feature.enabled?(:hide_merge_requests_from_banned_users)
+ where_not_exists(Users::BannedUser.where('merge_requests.author_id = banned_users.user_id'))
+ else
+ all
+ end
+ }
+
def self.total_time_to_merge
join_metrics
.merge(MergeRequest::Metrics.with_valid_time_to_merge)
@@ -2001,6 +2015,10 @@ class MergeRequest < ApplicationRecord
false # overridden in EE
end
+ def hidden?
+ Feature.enabled?(:hide_merge_requests_from_banned_users) && author&.banned?
+ end
+
private
attr_accessor :skip_fetch_ref
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index cff8911d84b..1395b8ff162 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -392,8 +392,13 @@ class MergeRequestDiff < ApplicationRecord
def diffs_in_batch(batch_page, batch_size, diff_options:)
fetching_repository_diffs(diff_options) do |comparison|
- reorder_diff_files!
- diffs_batch = diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options)
+ Gitlab::Metrics.measure(:diffs_reorder) do
+ reorder_diff_files!
+ end
+
+ diffs_batch = Gitlab::Metrics.measure(:diffs_collection) do
+ diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options)
+ end
if comparison
if diff_options[:paths].blank? && !without_files?
@@ -406,7 +411,9 @@ class MergeRequestDiff < ApplicationRecord
)
end
- comparison.diffs(diff_options)
+ Gitlab::Metrics.measure(:diffs_comparison) do
+ comparison.diffs(diff_options)
+ end
else
diffs_batch
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index da07d8dd9fc..b0676c25f8e 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -166,8 +166,6 @@ class Milestone < ApplicationRecord
end
def self.states_count(projects, groups = nil)
- return STATE_COUNT_HASH unless projects || groups
-
counts = Milestone
.for_projects_and_groups(projects, groups)
.reorder(nil)
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index f24161d598f..3ea46a8b703 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -2,6 +2,8 @@
module Ml
class Candidate < ApplicationRecord
+ PACKAGE_PREFIX = 'ml_candidate_'
+
enum status: { running: 0, scheduled: 1, finished: 2, failed: 3, killed: 4 }
validates :iid, :experiment, presence: true
@@ -16,20 +18,31 @@ module Ml
attribute :iid, default: -> { SecureRandom.uuid }
- scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
+ scope :including_relationships, -> { includes(:latest_metrics, :params, :user) }
+
+ delegate :project_id, :project, to: :experiment
def artifact_root
"/#{package_name}/#{package_version}/"
end
def artifact
- ::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version)
- rescue ActiveRecord::RecordNotFound
- nil
+ artifact_lazy&.itself
+ end
+
+ def artifact_lazy
+ BatchLoader.for(id).batch do |candidate_ids, loader|
+ Packages::Package
+ .joins("INNER JOIN ml_candidates ON packages_packages.name=(concat('#{PACKAGE_PREFIX}', ml_candidates.id))")
+ .where(ml_candidates: { id: candidate_ids })
+ .find_each do |package|
+ loader.call(package.name.delete_prefix(PACKAGE_PREFIX).to_i, package)
+ end
+ end
end
def package_name
- "ml_candidate_#{iid}"
+ "#{PACKAGE_PREFIX}#{id}"
end
def package_version
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index d7d53956656..cf638f9b16c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,17 +11,12 @@ class Namespace < ApplicationRecord
include FeatureGate
include FromUnion
include Gitlab::Utils::StrongMemoize
- include IgnorableColumns
include Namespaces::Traversal::Recursive
include Namespaces::Traversal::Linear
include EachBatch
include BlocksUnsafeSerialization
include Ci::NamespaceSettings
- # Temporary column used for back-filling project namespaces.
- # Remove it once the back-filling of all project namespaces is done.
- ignore_column :tmp_project_id, remove_with: '14.7', remove_after: '2022-01-22'
-
# Tells ActiveRecord not to store the full class name, in order to save some space
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
self.store_full_sti_class = false
@@ -33,9 +28,11 @@ class Namespace < ApplicationRecord
NUMBER_OF_ANCESTORS_ALLOWED = 20
SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable'
+ # DISABLED_WITH_OVERRIDE is deprecated in favour of DISABLED_AND_OVERRIDABLE.
SR_DISABLED_WITH_OVERRIDE = 'disabled_with_override'
+ SR_DISABLED_AND_OVERRIDABLE = 'disabled_and_overridable'
SR_ENABLED = 'enabled'
- SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_ENABLED].freeze
+ SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE, SR_ENABLED].freeze
URL_MAX_LENGTH = 255
PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze
@@ -87,6 +84,7 @@ class Namespace < ApplicationRecord
has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory'
has_many :achievements, class_name: 'Achievements::Achievement'
+ has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail'
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
@@ -134,6 +132,10 @@ class Namespace < ApplicationRecord
to: :namespace_settings
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
to: :namespace_settings
+ delegate :allow_runner_registration_token,
+ :allow_runner_registration_token?,
+ :allow_runner_registration_token=,
+ to: :namespace_settings
delegate :maven_package_requests_forwarding,
:pypi_package_requests_forwarding,
:npm_package_requests_forwarding,
@@ -556,7 +558,7 @@ class Namespace < ApplicationRecord
if shared_runners_enabled
SR_ENABLED
elsif allow_descendants_override_disabled_shared_runners
- SR_DISABLED_WITH_OVERRIDE
+ SR_DISABLED_AND_OVERRIDABLE
else
SR_DISABLED_AND_UNOVERRIDABLE
end
@@ -566,10 +568,10 @@ class Namespace < ApplicationRecord
case other_setting
when SR_ENABLED
false
- when SR_DISABLED_WITH_OVERRIDE
+ when SR_DISABLED_WITH_OVERRIDE, SR_DISABLED_AND_OVERRIDABLE
shared_runners_setting == SR_ENABLED
when SR_DISABLED_AND_UNOVERRIDABLE
- shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE
+ shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_AND_OVERRIDABLE || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE
else
raise ArgumentError
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 5081d5cdafe..7f65fb3a378 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -69,6 +69,12 @@ class NamespaceSetting < ApplicationRecord
!self.class.where(namespace_id: namespace.ancestors, runner_registration_enabled: false).exists?
end
+ def allow_runner_registration_token?
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ settings.allow_runner_registration_token && namespace.root_ancestor.allow_runner_registration_token
+ end
+
private
def all_ancestors_allow_diff_preview_in_email?
diff --git a/app/models/note.rb b/app/models/note.rb
index 052df6142c5..73c8e72d8b0 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -125,6 +125,7 @@ class Note < ApplicationRecord
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
scope :system, -> { where(system: true) }
scope :user, -> { where(system: false) }
+ scope :not_internal, -> { where(internal: false) }
scope :common, -> { where(noteable_type: ["", nil]) }
scope :fresh, -> { order_created_asc.with_order_id_asc }
scope :updated_after, ->(time) { where('updated_at > ?', time) }
@@ -133,9 +134,16 @@ class Note < ApplicationRecord
scope :inc_author, -> { includes(:author) }
scope :inc_note_diff_file, -> { includes(:note_diff_file) }
scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) }
- scope :inc_relations_for_view, -> do
- includes({ project: :group }, { author: :status }, :updated_by, :resolved_by, :award_emoji,
- { system_note_metadata: :description_version }, :note_diff_file, :diff_note_positions, :suggestions)
+ scope :inc_relations_for_view, ->(noteable = nil) do
+ relations = [{ project: :group }, { author: :status }, :updated_by, :resolved_by,
+ :award_emoji, { system_note_metadata: :description_version }, :suggestions]
+
+ if noteable.nil? || DiffNote.noteable_types.include?(noteable.class.name) ||
+ Feature.disabled?(:skip_notes_diff_include)
+ relations += [:note_diff_file, :diff_note_positions]
+ end
+
+ includes(relations)
end
scope :with_notes_filter, -> (notes_filter) do
diff --git a/app/models/packages/nuget.rb b/app/models/packages/nuget.rb
index 6bedd488c8a..9a9e5b6605a 100644
--- a/app/models/packages/nuget.rb
+++ b/app/models/packages/nuget.rb
@@ -3,6 +3,7 @@ module Packages
module Nuget
TEMPORARY_PACKAGE_NAME = 'NuGet.Temporary.Package'
TEMPORARY_SYMBOL_PACKAGE_NAME = 'NuGet.Temporary.SymbolPackage'
+ FORMAT = 'nupkg'
def self.table_name_prefix
'packages_nuget_'
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 17c5415939c..966165f9ad7 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -36,6 +36,7 @@ class Packages::Package < ApplicationRecord
# TODO: put the installable default scope on the :package_files association once the dependent: :destroy is removed
# See https://gitlab.com/gitlab-org/gitlab/-/issues/349191
has_many :installable_package_files, -> { installable }, class_name: 'Packages::PackageFile', inverse_of: :package
+ has_many :installable_nuget_package_files, -> { installable.with_nuget_format }, class_name: 'Packages::PackageFile', inverse_of: :package
has_many :dependency_links, inverse_of: :package, class_name: 'Packages::DependencyLink'
has_many :tags, inverse_of: :package, class_name: 'Packages::Tag'
has_one :conan_metadatum, inverse_of: :package, class_name: 'Packages::Conan::Metadatum'
@@ -128,6 +129,7 @@ class Packages::Package < ApplicationRecord
scope :including_project_namespace_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
+ scope :including_dependency_links_with_nuget_metadatum, -> { includes(dependency_links: [:dependency, :nuget_metadatum]) }
scope :with_conan_channel, ->(package_channel) do
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
@@ -149,12 +151,14 @@ class Packages::Package < ApplicationRecord
end
scope :preload_composer, -> { preload(:composer_metadatum) }
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
+ scope :preload_nuget_metadatum, -> { preload(:nuget_metadatum) }
scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
scope :has_version, -> { where.not(version: nil) }
scope :preload_files, -> { preload(:installable_package_files) }
+ scope :preload_nuget_files, -> { preload(:installable_nuget_package_files) }
scope :preload_pipelines, -> { preload(pipelines: :user) }
scope :last_of_each_version, -> { where(id: all.select('MAX(id) AS id').group(:version)) }
scope :limit_recent, ->(limit) { order_created_desc.limit(limit) }
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 3d56c563ec8..e1486c11298 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -44,6 +44,7 @@ class Packages::PackageFile < ApplicationRecord
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
scope :with_files_stored_locally, -> { where(file_store: ::Packages::PackageFileUploader::Store::LOCAL) }
scope :with_format, ->(format) { where(::Packages::PackageFile.arel_table[:file_name].matches("%.#{format}")) }
+ scope :with_nuget_format, -> { with_format(Packages::Nuget::FORMAT) }
scope :preload_package, -> { preload(:package) }
scope :preload_pipelines, -> { preload(pipelines: :user) }
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index cf0f0f9e92f..a1ba48f3ab0 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -46,7 +46,7 @@ module Pages
strong_memoize_attr :source
def prefix
- if project.pages_group_root?
+ if project.pages_namespace_url == project.pages_url
'/'
else
project.full_path.delete_prefix(trim_prefix) + '/'
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 4e3f4b0c328..909658214fd 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -78,6 +78,10 @@ class PagesDomain < ApplicationRecord
find_by("LOWER(domain) = LOWER(?)", domain)
end
+ def self.ids_for_project(project_id)
+ where(project_id: project_id).ids
+ end
+
def verified?
!!verified_at
end
@@ -209,7 +213,7 @@ class PagesDomain < ApplicationRecord
return unless pages_deployed?
cache = if Feature.enabled?(:cache_pages_domain_api, project.root_namespace)
- ::Gitlab::Pages::CacheControl.for_project(project.id)
+ ::Gitlab::Pages::CacheControl.for_domain(id)
end
Pages::VirtualDomain.new(
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 887ef36cc17..0da205f86a5 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -21,6 +21,11 @@ class PersonalAccessToken < ApplicationRecord
after_initialize :set_default_scopes, if: :persisted?
before_save :ensure_token
+ # During the implementation of Admin Mode for API, tokens of
+ # administrators should automatically get the `admin_mode` scope as well
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/42692
+ before_create :add_admin_mode_scope, if: :user_admin?
+
scope :active, -> { not_revoked.not_expired }
scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) }
scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) }
@@ -79,7 +84,12 @@ class PersonalAccessToken < ApplicationRecord
protected
def validate_scopes
- unless revoked || scopes.all? { |scope| Gitlab::Auth.all_available_scopes.include?(scope.to_sym) }
+ # During the implementation of Admin Mode for API,
+ # the `admin_mode` scope is not yet part of `all_available_scopes` but still valid.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/42692
+ valid_scopes = Gitlab::Auth.all_available_scopes + [Gitlab::Auth::ADMIN_MODE_SCOPE]
+
+ unless revoked || scopes.all? { |scope| valid_scopes.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
@@ -91,6 +101,14 @@ class PersonalAccessToken < ApplicationRecord
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
+
+ def user_admin?
+ user.admin? # rubocop: disable Cop/UserAdmin
+ end
+
+ def add_admin_mode_scope
+ self.scopes += [Gitlab::Auth::ADMIN_MODE_SCOPE.to_s]
+ end
end
PersonalAccessToken.prepend_mod_with('PersonalAccessToken')
diff --git a/app/models/project.rb b/app/models/project.rb
index 73dbb55a07b..561a842f23a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -170,6 +170,7 @@ class Project < ApplicationRecord
end
# Project integrations
+ has_one :apple_app_store_integration, class_name: 'Integrations::AppleAppStore'
has_one :asana_integration, class_name: 'Integrations::Asana'
has_one :assembla_integration, class_name: 'Integrations::Assembla'
has_one :bamboo_integration, class_name: 'Integrations::Bamboo'
@@ -269,6 +270,7 @@ class Project < ApplicationRecord
has_many :integrations
has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration'
+ has_many :incident_hooks_integrations, -> { incident_hooks }, class_name: 'Integration'
has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration'
has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration'
has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration'
@@ -291,18 +293,24 @@ class Project < ApplicationRecord
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
+
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
-
- has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id
-
alias_method :members, :project_members
- has_many :users, through: :project_members
+ has_many :namespace_members, ->(project) { where(requested_at: nil).unscope(where: %i[source_id source_type]) },
+ primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project, class_name: 'ProjectMember'
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :namespace_requesters, ->(project) { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) },
+ primary_key: :project_namespace_id, foreign_key: :member_namespace_id, inverse_of: :project, class_name: 'ProjectMember'
+
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
+ has_many :users, through: :project_members
+
+ has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id
+
has_many :deploy_keys_projects, inverse_of: :project
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects
@@ -750,16 +758,13 @@ class Project < ApplicationRecord
end
end
- # Defines instance methods:
+ # Define two instance methods:
#
- # - only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: false)
- # - allow_merge_on_skipped_pipeline?(inherit_group_setting: false)
- # - only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: false)
- # - only_allow_merge_if_pipeline_succeeds_locked?
- # - allow_merge_on_skipped_pipeline_locked?
- # - only_allow_merge_if_all_discussions_are_resolved_locked?
+ # - [attribute]?(inherit_group_setting) Returns the final value after inheriting the parent group
+ # - [attribute]_locked? Returns true if the value is inherited from the parent group
+ #
+ # These functions will be overridden in EE to make sense afterwards
def self.cascading_with_parent_namespace(attribute)
- # method overriden in EE
define_method("#{attribute}?") do |inherit_group_setting: false|
self.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -1610,7 +1615,9 @@ class Project < ApplicationRecord
end
def disabled_integrations
- []
+ disabled_integrations = []
+ disabled_integrations << 'apple_app_store' unless Feature.enabled?(:apple_app_store_integration, self)
+ disabled_integrations
end
def find_or_initialize_integration(name)
@@ -1722,14 +1729,8 @@ class Project < ApplicationRecord
def execute_integrations(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- if use_integration_relations?
- association("#{hooks_scope}_integrations").reader.each do |integration|
- integration.async_execute(data)
- end
- else
- integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
- integration.async_execute(data)
- end
+ association("#{hooks_scope}_integrations").reader.each do |integration|
+ integration.async_execute(data)
end
end
end
@@ -2100,7 +2101,7 @@ class Project < ApplicationRecord
pages_metadatum&.deployed?
end
- def pages_group_url
+ def pages_namespace_url
# The host in URL always needs to be downcased
Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{pages_subdomain}."
@@ -2108,19 +2109,23 @@ class Project < ApplicationRecord
end
def pages_url
- url = pages_group_url
+ url = pages_namespace_url
url_path = full_path.partition('/').last
+ namespace_url = "#{Settings.pages.protocol}://#{url_path}".downcase
+
+ if Rails.env.development?
+ url_without_port = URI.parse(url)
+ url_without_port.port = nil
+
+ return url if url_without_port.to_s == namespace_url
+ end
# If the project path is the same as host, we serve it as group page
- return url if url == "#{Settings.pages.protocol}://#{url_path}".downcase
+ return url if url == namespace_url
"#{url}/#{url_path}"
end
- def pages_group_root?
- pages_group_url == pages_url
- end
-
def pages_subdomain
full_path.partition('/').first
end
@@ -2920,12 +2925,6 @@ class Project < ApplicationRecord
Gitlab::Routing.url_helpers.activity_project_path(self)
end
- def increment_statistic_value(statistic, delta)
- return if pending_delete?
-
- ProjectStatistics.increment_statistic(self, statistic, delta)
- end
-
def ci_forward_deployment_enabled?
return false unless ci_cd_settings
@@ -3369,12 +3368,6 @@ class Project < ApplicationRecord
ProjectFeature::PRIVATE
end
end
-
- def use_integration_relations?
- strong_memoize(:use_integration_relations) do
- Feature.enabled?(:cache_project_integrations, self)
- end
- end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index 7116ccd9824..db86bb5e1fb 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -59,7 +59,7 @@ class ProjectSetting < ApplicationRecord
!!super
end
end
- strong_memoize_attr :show_diff_preview_in_email?, :show_diff_preview_in_email
+ strong_memoize_attr :show_diff_preview_in_email?
private
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 506f6305791..732dadc03d9 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -123,16 +123,37 @@ class ProjectStatistics < ApplicationRecord
# through counter_attribute_after_commit
#
# For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS
- def self.increment_statistic(project, key, amount)
+ def self.increment_statistic(project, key, increment)
+ return if project.pending_delete?
+
+ project.statistics.try do |project_statistics|
+ project_statistics.increment_statistic(key, increment)
+ end
+ end
+
+ def self.bulk_increment_statistic(project, key, increments)
+ unless Feature.enabled?(:project_statistics_bulk_increment, type: :development)
+ total_amount = Gitlab::Counters::Increment.new(amount: increments.sum(&:amount))
+ return increment_statistic(project, key, total_amount)
+ end
+
+ return if project.pending_delete?
+
project.statistics.try do |project_statistics|
- project_statistics.increment_statistic(key, amount)
+ project_statistics.bulk_increment_statistic(key, increments)
end
end
- def increment_statistic(key, amount)
+ def increment_statistic(key, increment)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
+
+ increment_counter(key, increment)
+ end
+
+ def bulk_increment_statistic(key, increments)
raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
- increment_counter(key, amount)
+ bulk_increment_counter(key, increments)
end
private
diff --git a/app/models/projects/branch_rule.rb b/app/models/projects/branch_rule.rb
new file mode 100644
index 00000000000..ae59d24e557
--- /dev/null
+++ b/app/models/projects/branch_rule.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Projects
+ class BranchRule
+ extend Forwardable
+
+ attr_reader :project, :protected_branch
+
+ def_delegators(:protected_branch, :name, :group, :default_branch?, :created_at, :updated_at)
+
+ def initialize(project, protected_branch)
+ @protected_branch = protected_branch
+ @project = project
+ end
+
+ def protected?
+ true
+ end
+
+ def matching_branches_count
+ branch_names = project.repository.branch_names
+ matching_branches = protected_branch.matching(branch_names)
+ matching_branches.count
+ end
+
+ def branch_protection
+ protected_branch
+ end
+ end
+end
+
+Projects::BranchRule.prepend_mod
diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb
index 2ffc7478178..b791cb1254c 100644
--- a/app/models/projects/build_artifacts_size_refresh.rb
+++ b/app/models/projects/build_artifacts_size_refresh.rb
@@ -7,16 +7,34 @@ module Projects
STALE_WINDOW = 2.hours
+ # This delay is set to 10 minutes to accommodate any ongoing
+ # deletion that might have happened.
+ # The delete on the database may have been committed before
+ # the refresh completed its batching. If the resulting decrement is
+ # pushed into Redis after the refresh has ended, it would result in net negative value.
+ # The delay is needed to ensure this negative value is ignored.
+ FINALIZE_DELAY = 10.minutes
+
self.table_name = 'project_build_artifacts_size_refreshes'
+ COUNTER_ATTRIBUTE_NAME = :build_artifacts_size
+
belongs_to :project
validates :project, presence: true
+ # The refresh of the project statistics counter is performed in 4 stages:
+ # 1. created - The refresh is on the queue to be processed by Projects::RefreshBuildArtifactsSizeStatisticsWorker
+ # 2. running - The refresh is ongoing. The project statistics counter switches to the temporary refresh counter key.
+ # Counter increments are deduplicated.
+ # 3. pending - The refresh is pending to be picked up by Projects::RefreshBuildArtifactsSizeStatisticsWorker again.
+ # 4. finalizing - The refresh has finished summing existing job artifact size into the refresh counter key.
+ # The sum will need to be moved into the counter key.
STATES = {
created: 1,
running: 2,
- pending: 3
+ pending: 3,
+ finalizing: 4
}.freeze
state_machine :state, initial: :created do
@@ -24,6 +42,7 @@ module Projects
state :created, value: STATES[:created]
state :running, value: STATES[:running]
state :pending, value: STATES[:pending]
+ state :finalizing, value: STATES[:finalizing]
event :process do
transition [:created, :pending, :running] => :running
@@ -33,7 +52,10 @@ module Projects
transition running: :pending
end
- # set it only the first time we execute the refresh
+ event :schedule_finalize do
+ transition running: :finalizing
+ end
+
before_transition created: :running do |refresh|
refresh.reset_project_statistics!
refresh.refresh_started_at = Time.zone.now
@@ -47,6 +69,10 @@ module Projects
before_transition running: :pending do |refresh, transition|
refresh.last_job_artifact_id = transition.args.first
end
+
+ before_transition running: :finalizing do |refresh, transition|
+ refresh.schedule_finalize_worker
+ end
end
scope :stale, -> { with_state(:running).where('updated_at < ?', STALE_WINDOW.ago) }
@@ -80,7 +106,7 @@ module Projects
end
def reset_project_statistics!
- project.statistics.reset_counter!(:build_artifacts_size)
+ project.statistics.initiate_refresh!(COUNTER_ATTRIBUTE_NAME)
end
def next_batch(limit:)
@@ -95,6 +121,18 @@ module Projects
!created?
end
+ def finalize!
+ project.statistics.finalize_refresh(COUNTER_ATTRIBUTE_NAME)
+
+ destroy!
+ end
+
+ def schedule_finalize_worker
+ run_after_commit do
+ Projects::FinalizeProjectStatisticsRefreshWorker.perform_in(FINALIZE_DELAY, self.class.to_s, id)
+ end
+ end
+
private
def schedule_namespace_aggregation_worker
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index c59ef4cd80b..050db3b6870 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -8,11 +8,9 @@ class ProtectedBranch < ApplicationRecord
validate :validate_either_project_or_top_group
- scope :requiring_code_owner_approval,
- -> { where(code_owner_approval_required: true) }
-
- scope :allowing_force_push,
- -> { where(allow_force_push: true) }
+ scope :requiring_code_owner_approval, -> { where(code_owner_approval_required: true) }
+ scope :allowing_force_push, -> { where(allow_force_push: true) }
+ scope :sorted_by_name, -> { order(name: :asc) }
protected_ref_access_levels :merge, :push
@@ -106,6 +104,10 @@ class ProtectedBranch < ApplicationRecord
name == project.default_branch
end
+ def entity
+ group || project
+ end
+
private
def validate_either_project_or_top_group
@@ -113,7 +115,7 @@ class ProtectedBranch < ApplicationRecord
errors.add(:base, _('must be associated with a Group or a Project'))
elsif project && group
errors.add(:base, _('cannot be associated with both a Group and a Project'))
- elsif group && group.root_ancestor != group
+ elsif group && group.subgroup?
errors.add(:base, _('cannot be associated with a subgroup'))
end
end
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index df75c557717..76e620aa3bf 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProtectedBranch::MergeAccessLevel < ApplicationRecord
+ include Importable
include ProtectedBranchAccess
# default value for the access_level column
GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 6076fab20b7..66fe57be25f 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProtectedBranch::PushAccessLevel < ApplicationRecord
+ include Importable
include ProtectedBranchAccess
# default value for the access_level column
GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
index 9fcfa7646a2..5d8b1fb4f71 100644
--- a/app/models/protected_tag/create_access_level.rb
+++ b/app/models/protected_tag/create_access_level.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class ProtectedTag::CreateAccessLevel < ApplicationRecord
+ include Importable
include ProtectedTagAccess
def check_access(user)
diff --git a/app/models/release.rb b/app/models/release.rb
index 5ef3ff1bc6c..b770f3934ef 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -26,12 +26,13 @@ class Release < ApplicationRecord
before_create :set_released_at
validates :project, :tag, presence: true
+ validates :author_id, presence: true, if: :validate_release_with_author?
+
validates :tag, uniqueness: { scope: :project_id }
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :description_changed?
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
- validates :author_id, presence: true, on: :create, if: :validate_release_with_author?
scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> {
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 90e87de4a5b..cedfed16b20 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -189,6 +189,8 @@ class Repository
return []
end
+ query = Feature.enabled?(:commit_search_trailing_spaces) ? query.strip : query
+
commits = raw_repository.find_commits_by_message(query, ref, path, limit, offset).map do |c|
commit(c)
end
@@ -631,7 +633,11 @@ class Repository
end
def readme_path
- head_tree&.readme_path
+ if Feature.enabled?(:readme_from_gitaly)
+ readme_path_gitaly
+ else
+ head_tree&.readme_path
+ end
end
cache_method :readme_path
@@ -1239,6 +1245,29 @@ class Repository
container.full_path,
container: container)
end
+
+ def readme_path_gitaly
+ return if empty? || root_ref.nil?
+
+ # (?i) to enable case-insensitive mode
+ #
+ # Note: `Gitlab::FileDetector::PATTERNS[:readme]#to_s` won't work because of
+ # incompatibility of regex engines between Rails and Gitaly.
+ regex = "(?i)#{Gitlab::FileDetector::PATTERNS[:readme].source}"
+
+ readmes = search_files_by_regexp(regex, root_ref)
+
+ choose_readme_to_display(readmes)
+ end
+
+ # Extracted from Tree#readme_path
+ def choose_readme_to_display(readmes)
+ previewable_readme = readmes.find { |name| Gitlab::MarkupHelper.previewable?(name) }
+
+ return previewable_readme if previewable_readme
+
+ readmes.find { |name| Gitlab::MarkupHelper.plain?(name) }
+ end
end
Repository.prepend_mod_with('Repository')
diff --git a/app/models/resource_event.rb b/app/models/resource_event.rb
index 8b82e0f343c..551ea984132 100644
--- a/app/models/resource_event.rb
+++ b/app/models/resource_event.rb
@@ -3,6 +3,8 @@
class ResourceEvent < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Importable
+ include IssueResourceEvent
+ include WorkItemResourceEvent
self.abstract_class = true
@@ -18,6 +20,10 @@ class ResourceEvent < ApplicationRecord
end
end
+ def issuable
+ raise NoMethodError, 'must implement `issuable` method'
+ end
+
private
def discussion_id_key
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index a1426540cf5..efffc1bd6dc 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -2,7 +2,6 @@
class ResourceLabelEvent < ResourceEvent
include CacheMarkdownField
- include IssueResourceEvent
include MergeRequestResourceEvent
cache_markdown_field :reference
@@ -39,6 +38,10 @@ class ResourceLabelEvent < ResourceEvent
issue || merge_request
end
+ def synthetic_note_class
+ LabelNote
+ end
+
def project
issuable.project
end
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index 5fd71612de0..def7e91af3f 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -19,4 +19,8 @@ class ResourceMilestoneEvent < ResourceTimeboxEvent
def milestone_parent
milestone&.parent
end
+
+ def synthetic_note_class
+ MilestoneNote
+ end
end
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
index 6ebb9d5f176..134f71e35ad 100644
--- a/app/models/resource_state_event.rb
+++ b/app/models/resource_state_event.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class ResourceStateEvent < ResourceEvent
- include IssueResourceEvent
include MergeRequestResourceEvent
include Importable
@@ -26,6 +25,10 @@ class ResourceStateEvent < ResourceEvent
issue_id.present?
end
+ def synthetic_note_class
+ StateNote
+ end
+
private
def issue_usage_metrics
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index 26bf2a225d4..dddd4d0fe84 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -1,12 +1,11 @@
# frozen_string_literal: true
class ResourceTimeboxEvent < ResourceEvent
- self.abstract_class = true
-
- include IssueResourceEvent
include MergeRequestResourceEvent
include Importable
+ self.abstract_class = true
+
validate :exactly_one_issuable, unless: :importing?
enum action: {
diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb
index a60c0d2f3bc..f88fa052665 100644
--- a/app/models/synthetic_note.rb
+++ b/app/models/synthetic_note.rb
@@ -14,7 +14,7 @@ class SyntheticNote < Note
discussion_id: event.discussion_id,
noteable: resource,
event: event,
- system_note_metadata: ::SystemNoteMetadata.new(action: action),
+ system_note_metadata: ::SystemNoteMetadata.new(action: action, id: event.discussion_id),
resource_parent: resource_parent
}
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 4e86036952b..36166bdbc9a 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -34,6 +34,12 @@ class SystemNoteMetadata < ApplicationRecord
belongs_to :note
belongs_to :description_version
+ delegate_missing_to :note
+
+ def declarative_policy_delegate
+ note
+ end
+
def icon_types
ICON_TYPES
end
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index 7c394736560..07c61f64f29 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -35,10 +35,21 @@ class Timelog < ApplicationRecord
where('spent_at <= ?', end_time)
end
+ scope :order_scope_asc, ->(field) { order(arel_table[field].asc.nulls_last) }
+ scope :order_scope_desc, ->(field) { order(arel_table[field].desc.nulls_last) }
+
def issuable
issue || merge_request
end
+ def self.sort_by_field(field, direction)
+ if direction == :asc
+ order_scope_asc(field)
+ else
+ order_scope_desc(field)
+ end
+ end
+
private
def issuable_id_is_present
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 32ec4accb4b..7bbdf321269 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -204,10 +204,18 @@ class Todo < ApplicationRecord
action == MEMBER_ACCESS_REQUESTED
end
- def access_request_url
- return "" unless self.target_type == 'Namespace'
+ def member_access_type
+ target.class.name.downcase
+ end
- Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests')
+ def access_request_url(only_path: false)
+ if target.instance_of? Group
+ Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests', only_path: only_path)
+ elsif target.instance_of? Project
+ Gitlab::Routing.url_helpers.project_project_members_url(self.target, tab: 'access_requests', only_path: only_path)
+ else
+ ""
+ end
end
def done?
diff --git a/app/models/user.rb b/app/models/user.rb
index ba3f7922c9c..da6e1abad07 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,12 +63,13 @@ class User < ApplicationRecord
attribute :admin, default: false
attribute :external, default: -> { Gitlab::CurrentSettings.user_default_external }
attribute :can_create_group, default: -> { Gitlab::CurrentSettings.can_create_group }
+ attribute :private_profile, default: -> { Gitlab::CurrentSettings.user_defaults_to_private_profile }
attribute :can_create_team, default: false
attribute :hide_no_ssh_key, default: false
attribute :hide_no_password, default: false
attribute :project_view, default: :files
attribute :notified_of_own_activity, default: false
- attribute :preferred_language, default: -> { I18n.default_locale }
+ attribute :preferred_language, default: -> { Gitlab::CurrentSettings.default_preferred_language }
attribute :theme_id, default: -> { gitlab_config.default_theme }
attr_encrypted :otp_secret,
@@ -100,6 +101,8 @@ class User < ApplicationRecord
MINIMUM_DAYS_CREATED = 7
+ ignore_columns %i[linkedin twitter skype website_url location organization], remove_with: '15.8', remove_after: '2023-01-22'
+
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
@@ -214,7 +217,7 @@ class User < ApplicationRecord
has_many :releases, dependent: :nullify, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :subscriptions, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :abuse_reports, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, class_name: 'Ci::Build'
@@ -262,8 +265,11 @@ class User < ApplicationRecord
has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
-
- has_many :namespace_commit_emails
+ has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail'
+ has_many :user_achievements, class_name: 'Achievements::UserAchievement', inverse_of: :user
+ has_many :awarded_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'awarded_by_user_id', inverse_of: :awarded_by_user
+ has_many :revoked_user_achievements, class_name: 'Achievements::UserAchievement', foreign_key: 'revoked_by_user_id', inverse_of: :revoked_by_user
+ has_many :achievements, through: :user_achievements, class_name: 'Achievements::Achievement', inverse_of: :users
#
# Validations
@@ -298,19 +304,15 @@ class User < ApplicationRecord
validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids,
message: ->(*) { _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } } }
- validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
-
after_initialize :set_projects_limit
before_validation :sanitize_attrs
before_validation :ensure_namespace_correct
after_validation :set_username_errors
- before_save :default_private_profile_to_false
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_save :ensure_namespace_correct # in case validation is skipped
- before_save :ensure_user_detail_assigned
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
@@ -372,6 +374,12 @@ class User < ApplicationRecord
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
delegate :requires_credit_card_verification, :requires_credit_card_verification=, 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
+ delegate :website_url, :website_url=, to: :user_detail, allow_nil: true
+ delegate :location, :location=, to: :user_detail, allow_nil: true
+ delegate :organization, :organization=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -531,9 +539,7 @@ class User < ApplicationRecord
strip_attributes! :name
def preferred_language
- read_attribute('preferred_language') ||
- I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) ||
- default_preferred_language
+ read_attribute('preferred_language').presence || Gitlab::CurrentSettings.default_preferred_language
end
def active_for_authentication?
@@ -1401,17 +1407,9 @@ class User < ApplicationRecord
end
def sanitize_attrs
- sanitize_links
sanitize_name
end
- def sanitize_links
- %i[skype linkedin twitter].each do |attr|
- value = self[attr]
- self[attr] = Sanitize.clean(value) if value.present?
- end
- end
-
def sanitize_name
return unless self.name
@@ -1595,11 +1593,6 @@ class User < ApplicationRecord
end
end
- # Temporary, will be removed when user_detail fields are fully migrated
- def ensure_user_detail_assigned
- user_detail.assign_changed_fields_from_user if UserDetail.user_fields_changed?(self)
- end
-
def set_username_errors
namespace_path_errors = self.errors.delete(:"namespace.path")
@@ -1890,7 +1883,7 @@ class User < ApplicationRecord
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
- Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) if Feature.enabled?(:limit_assigned_issues_count)
+ Rails.cache.delete(['users', id, 'max_assigned_open_issues_count'])
end
def invalidate_merge_request_cache_counts
@@ -2189,6 +2182,13 @@ class User < ApplicationRecord
public_email.presence || _('[REDACTED]')
end
+ def namespace_commit_email_for_project(project)
+ return if project.nil?
+
+ namespace_commit_emails.find_by(namespace: project.project_namespace) ||
+ namespace_commit_emails.find_by(namespace: project.root_namespace)
+ end
+
protected
# override, from Devise::Validatable
@@ -2230,11 +2230,6 @@ class User < ApplicationRecord
otp_backup_codes.first.start_with?("$pbkdf2-sha512$")
end
- # To enable JiHu repository to modify the default language options
- def default_preferred_language
- 'en'
- end
-
# rubocop: disable CodeReuse/ServiceClass
def add_primary_email_to_emails!
Emails::CreateService.new(self, user: self, email: self.email).execute(confirmed_at: self.confirmed_at)
@@ -2299,12 +2294,6 @@ class User < ApplicationRecord
])
end
- def default_private_profile_to_false
- return unless private_profile_changed? && private_profile.nil?
-
- self.private_profile = false
- end
-
def has_current_license?
false
end
diff --git a/app/models/user_custom_attribute.rb b/app/models/user_custom_attribute.rb
index 559e93be360..4ebb8ba9f00 100644
--- a/app/models/user_custom_attribute.rb
+++ b/app/models/user_custom_attribute.rb
@@ -11,6 +11,9 @@ class UserCustomAttribute < ApplicationRecord
scope :by_updated_at, ->(updated_at) { where(updated_at: updated_at) }
scope :arkose_sessions, -> { by_key('arkose_session') }
+ BLOCKED_BY = 'blocked_by'
+ UNBLOCKED_BY = 'unblocked_by'
+
class << self
def upsert_custom_attributes(custom_attributes)
created_at = DateTime.now
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 0570bc2f395..b6765cb0285 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -22,14 +22,10 @@ class UserDetail < ApplicationRecord
validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed?
before_validation :sanitize_attrs
- before_save :prevent_nil_bio
+ before_save :prevent_nil_fields
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
- def self.user_fields_changed?(user)
- (%w[linkedin skype twitter website_url location organization] & user.changed).any?
- end
-
def sanitize_attrs
%i[linkedin skype twitter website_url].each do |attr|
value = self[attr]
@@ -41,25 +37,16 @@ class UserDetail < ApplicationRecord
end
end
- def assign_changed_fields_from_user
- self.linkedin = trim_field(user.linkedin) if user.linkedin_changed?
- self.twitter = trim_field(user.twitter) if user.twitter_changed?
- self.skype = trim_field(user.skype) if user.skype_changed?
- self.website_url = trim_field(user.website_url) if user.website_url_changed?
- self.location = trim_field(user.location) if user.location_changed?
- self.organization = trim_field(user.organization) if user.organization_changed?
- end
-
private
- def prevent_nil_bio
- self.bio = '' if bio_changed? && bio.nil?
- end
-
- def trim_field(value)
- return '' unless value
-
- value.first(DEFAULT_FIELD_LENGTH)
+ def prevent_nil_fields
+ self.bio = '' if bio.nil?
+ self.linkedin = '' if linkedin.nil?
+ self.twitter = '' if twitter.nil?
+ self.skype = '' if skype.nil?
+ self.location = '' if location.nil?
+ self.organization = '' if organization.nil?
+ self.website_url = '' if website_url.nil?
end
end
diff --git a/app/models/users/namespace_commit_email.rb b/app/models/users/namespace_commit_email.rb
index 4ec02f12717..883b17187ca 100644
--- a/app/models/users/namespace_commit_email.rb
+++ b/app/models/users/namespace_commit_email.rb
@@ -9,6 +9,22 @@ module Users
validates :user, presence: true
validates :namespace, presence: true
validates :email, presence: true
- validates :user_id, uniqueness: { scope: [:namespace_id] }
+ validates :user, uniqueness: { scope: :namespace_id }
+ validate :validate_root_group
+
+ def self.delete_for_namespace(namespace)
+ where(namespace: namespace).delete_all
+ end
+
+ private
+
+ def validate_root_group
+ # Due to the way Rails validations are invoked all at once,
+ # namespace sometimes won't exist when this is ran even though we have a validation for presence first.
+ return unless namespace&.group_namespace?
+ return if namespace.root?
+
+ errors.add(:namespace, _('must be a root group.'))
+ end
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index 0810c520f7e..f94e831437a 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -13,6 +13,8 @@ class WorkItem < Issue
has_many :child_links, class_name: '::WorkItems::ParentLink', foreign_key: :work_item_parent_id
has_many :work_item_children, through: :child_links, class_name: 'WorkItem',
foreign_key: :work_item_id, source: :work_item
+ has_many :work_item_children_by_created_at, -> { order(:created_at) }, through: :child_links, class_name: 'WorkItem',
+ foreign_key: :work_item_id, source: :work_item
scope :inc_relations_for_permission_check, -> { includes(:author, project: :project_feature) }
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 33857fb08c2..21e31980fda 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -2,6 +2,8 @@
module WorkItems
class ParentLink < ApplicationRecord
+ include RelativePositioning
+
self.table_name = 'work_item_parent_links'
MAX_CHILDREN = 100
@@ -31,6 +33,14 @@ module WorkItems
link.work_item_parent.confidential?
end
+
+ def relative_positioning_query_base(parent_link)
+ where(work_item_parent_id: parent_link.work_item_parent_id)
+ end
+
+ def relative_positioning_parent_column
+ :work_item_parent_id
+ end
end
private
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index d0819076efd..ee10c631bcc 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -8,7 +8,7 @@ module WorkItems
end
def children
- work_item.work_item_children
+ work_item.work_item_children_by_created_at
end
end
end