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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /app/models/concerns
parent4b1de649d0168371549608993deac953eb692019 (diff)
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/cache_markdown_field.rb62
-rw-r--r--app/models/concerns/can_move_repository_storage.rb46
-rw-r--r--app/models/concerns/case_sensitivity.rb12
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb9
-rw-r--r--app/models/concerns/enums/data_visualization_palette.rb33
-rw-r--r--app/models/concerns/enums/internal_id.rb3
-rw-r--r--app/models/concerns/has_repository.rb2
-rw-r--r--app/models/concerns/has_wiki_page_meta_attributes.rb164
-rw-r--r--app/models/concerns/has_wiki_page_slug_attributes.rb25
-rw-r--r--app/models/concerns/ignorable_columns.rb12
-rw-r--r--app/models/concerns/issuable.rb9
-rw-r--r--app/models/concerns/mentionable.rb41
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb37
-rw-r--r--app/models/concerns/project_features_compatibility.rb8
-rw-r--r--app/models/concerns/protected_ref.rb6
-rw-r--r--app/models/concerns/protected_ref_access.rb1
-rw-r--r--app/models/concerns/reactive_caching.rb3
-rw-r--r--app/models/concerns/repository_storage_movable.rb121
-rw-r--r--app/models/concerns/routable.rb47
-rw-r--r--app/models/concerns/shardable.rb1
-rw-r--r--app/models/concerns/timebox.rb6
-rw-r--r--app/models/concerns/token_authenticatable.rb7
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb10
-rw-r--r--app/models/concerns/triggerable_hooks.rb5
24 files changed, 576 insertions, 94 deletions
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 49fc780f372..45944401c2d 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -70,8 +70,12 @@ module CacheMarkdownField
def refresh_markdown_cache!
updates = refresh_markdown_cache
-
- save_markdown(updates)
+ if updates.present? && save_markdown(updates)
+ # save_markdown updates DB columns directly, so compute and save mentions
+ # by calling store_mentions! or we end-up with missing mentions although those
+ # would appear in the notes, descriptions, etc in the UI
+ store_mentions! if mentionable_attributes_changed?(updates)
+ end
end
def cached_html_up_to_date?(markdown_field)
@@ -106,7 +110,19 @@ module CacheMarkdownField
def updated_cached_html_for(markdown_field)
return unless cached_markdown_fields.markdown_fields.include?(markdown_field)
- refresh_markdown_cache! if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))
+ if attribute_invalidated?(cached_markdown_fields.html_field(markdown_field))
+ # Invalidated due to Markdown content change
+ # We should not persist the updated HTML here since this will depend on whether the
+ # Markdown content change will be persisted. Both will be persisted together when the model is saved.
+ if changed_attributes.key?(markdown_field)
+ refresh_markdown_cache
+ else
+ # Invalidated due to stale HTML cache
+ # This could happen when the Markdown cache version is bumped or when a model is imported and the HTML is empty.
+ # We persist the updated HTML here so that subsequent calls to this method do not have to regenerate the HTML again.
+ refresh_markdown_cache!
+ end
+ end
cached_html_for(markdown_field)
end
@@ -140,6 +156,46 @@ module CacheMarkdownField
nil
end
+ def store_mentions!
+ refs = all_references(self.author)
+
+ references = {}
+ references[:mentioned_users_ids] = refs.mentioned_users&.pluck(:id).presence
+ references[:mentioned_groups_ids] = refs.mentioned_groups&.pluck(:id).presence
+ references[:mentioned_projects_ids] = refs.mentioned_projects&.pluck(:id).presence
+
+ # One retry is enough as next time `model_user_mention` should return the existing mention record,
+ # that threw the `ActiveRecord::RecordNotUnique` exception in first place.
+ self.class.safe_ensure_unique(retries: 1) do
+ user_mention = model_user_mention
+
+ # this may happen due to notes polymorphism, so noteable_id may point to a record
+ # that no longer exists as we cannot have FK on noteable_id
+ break if user_mention.blank?
+
+ user_mention.mentioned_users_ids = references[:mentioned_users_ids]
+ user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
+ user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
+
+ if user_mention.has_mentions?
+ user_mention.save!
+ else
+ user_mention.destroy!
+ end
+ end
+
+ true
+ end
+
+ def mentionable_attributes_changed?(changes = saved_changes)
+ return false unless is_a?(Mentionable)
+
+ self.class.mentionable_attrs.any? do |attr|
+ changes.key?(cached_markdown_fields.html_field(attr.first)) &&
+ changes.fetch(cached_markdown_fields.html_field(attr.first)).last.present?
+ end
+ end
+
included do
cattr_reader :cached_markdown_fields do
Gitlab::MarkdownCache::FieldData.new
diff --git a/app/models/concerns/can_move_repository_storage.rb b/app/models/concerns/can_move_repository_storage.rb
new file mode 100644
index 00000000000..52c3a4106e3
--- /dev/null
+++ b/app/models/concerns/can_move_repository_storage.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module CanMoveRepositoryStorage
+ extend ActiveSupport::Concern
+
+ RepositoryReadOnlyError = Class.new(StandardError)
+
+ # Tries to set repository as read_only, checking for existing Git transfers in
+ # progress beforehand. Setting a repository read-only will fail if it is
+ # already in that state.
+ #
+ # @return nil. Failures will raise an exception
+ def set_repository_read_only!(skip_git_transfer_check: false)
+ with_lock do
+ raise RepositoryReadOnlyError, _('Git transfer in progress') if
+ !skip_git_transfer_check && git_transfer_in_progress?
+
+ raise RepositoryReadOnlyError, _('Repository already read-only') if
+ self.class.where(id: id).pick(:repository_read_only)
+
+ raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
+ update_column(:repository_read_only, true)
+
+ nil
+ end
+ end
+
+ # Set repository as writable again. Unlike setting it read-only, this will
+ # succeed if the repository is already writable.
+ def set_repository_writable!
+ with_lock do
+ raise ActiveRecord::RecordNotSaved, _('Database update failed') unless
+ update_column(:repository_read_only, false)
+
+ nil
+ end
+ end
+
+ def git_transfer_in_progress?
+ reference_counter(type: repository.repo_type).value > 0
+ end
+
+ def reference_counter(type:)
+ Gitlab::ReferenceCounter.new(type.identifier_for_container(self))
+ end
+end
diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb
index abddbf1c7e3..31b5afd604d 100644
--- a/app/models/concerns/case_sensitivity.rb
+++ b/app/models/concerns/case_sensitivity.rb
@@ -11,12 +11,14 @@ module CaseSensitivity
def iwhere(params)
criteria = self
- params.each do |key, value|
+ params.each do |column, value|
+ column = arel_table[column] unless column.is_a?(Arel::Attribute)
+
criteria = case value
when Array
- criteria.where(value_in(key, value))
+ criteria.where(value_in(column, value))
else
- criteria.where(value_equal(key, value))
+ criteria.where(value_equal(column, value))
end
end
@@ -28,7 +30,7 @@ module CaseSensitivity
def value_equal(column, value)
lower_value = lower_value(value)
- lower_column(arel_table[column]).eq(lower_value).to_sql
+ lower_column(column).eq(lower_value).to_sql
end
def value_in(column, values)
@@ -36,7 +38,7 @@ module CaseSensitivity
lower_value(value)
end
- lower_column(arel_table[column]).in(lower_values).to_sql
+ lower_column(column).in(lower_values).to_sql
end
def lower_value(value)
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index bb8df37f649..e1f07fa162c 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -9,7 +9,8 @@ module Enums
{
unknown_failure: 0,
config_error: 1,
- external_validation_failure: 2
+ external_validation_failure: 2,
+ deployments_limit_exceeded: 23
}
end
@@ -24,8 +25,6 @@ module Enums
schedule: 4,
api: 5,
external: 6,
- # TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
- # https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7,
chat: 8,
webide: 9,
@@ -53,6 +52,10 @@ module Enums
sources.except(*dangling_sources.keys)
end
+ def self.ci_branch_sources
+ ci_sources.except(:merge_request_event)
+ end
+
def self.ci_and_parent_sources
ci_sources.merge(sources.slice(:parent_pipeline))
end
diff --git a/app/models/concerns/enums/data_visualization_palette.rb b/app/models/concerns/enums/data_visualization_palette.rb
new file mode 100644
index 00000000000..25002e64ba6
--- /dev/null
+++ b/app/models/concerns/enums/data_visualization_palette.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Enums
+ # These color palettes are part of the Pajamas Design System.
+ # See https://design.gitlab.com/data-visualization/color/#categorical-data
+ module DataVisualizationPalette
+ def self.colors
+ {
+ blue: 0,
+ orange: 1,
+ aqua: 2,
+ green: 3,
+ magenta: 4
+ }
+ end
+
+ def self.weights
+ {
+ '50' => 0,
+ '100' => 1,
+ '200' => 2,
+ '300' => 3,
+ '400' => 4,
+ '500' => 5,
+ '600' => 6,
+ '700' => 7,
+ '800' => 8,
+ '900' => 9,
+ '950' => 10
+ }
+ end
+ end
+end
diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb
index f01bd60ef16..b08c05b1934 100644
--- a/app/models/concerns/enums/internal_id.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -15,7 +15,8 @@ module Enums
operations_user_lists: 7,
alert_management_alerts: 8,
sprints: 9, # iterations
- design_management_designs: 10
+ design_management_designs: 10,
+ incident_management_oncall_schedules: 11
}
end
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 3dea4a9f5fb..9692941d8b2 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -88,7 +88,7 @@ module HasRepository
group_branch_default_name = group&.default_branch_name if respond_to?(:group)
- group_branch_default_name || Gitlab::CurrentSettings.default_branch_name
+ (group_branch_default_name || Gitlab::CurrentSettings.default_branch_name).presence
end
def reload_default_branch
diff --git a/app/models/concerns/has_wiki_page_meta_attributes.rb b/app/models/concerns/has_wiki_page_meta_attributes.rb
new file mode 100644
index 00000000000..136f2d00ce3
--- /dev/null
+++ b/app/models/concerns/has_wiki_page_meta_attributes.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+module HasWikiPageMetaAttributes
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
+ WikiPageInvalid = Class.new(ArgumentError)
+
+ included do
+ has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+
+ validates :title, length: { maximum: 255 }, allow_nil: false
+ validate :no_two_metarecords_in_same_container_can_have_same_canonical_slug
+
+ scope :with_canonical_slug, ->(slug) do
+ slug_table_name = klass.reflect_on_association(:slugs).table_name
+
+ joins(:slugs).where(slug_table_name => { canonical: true, slug: slug })
+ end
+ end
+
+ class_methods do
+ # Return the (updated) WikiPage::Meta record for a given wiki page
+ #
+ # If none is found, then a new record is created, and its fields are set
+ # to reflect the wiki_page passed.
+ #
+ # @param [String] last_known_slug
+ # @param [WikiPage] wiki_page
+ #
+ # This method raises errors on validation issues.
+ def find_or_create(last_known_slug, wiki_page)
+ raise WikiPageInvalid unless wiki_page.valid?
+
+ container = wiki_page.wiki.container
+ known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
+ raise 'No slugs found! This should not be possible.' if known_slugs.empty?
+
+ transaction do
+ updates = wiki_page_updates(wiki_page)
+ found = find_by_canonical_slug(known_slugs, container)
+ meta = found || create!(updates.merge(container_attrs(container)))
+
+ meta.update_state(found.nil?, known_slugs, wiki_page, updates)
+
+ # We don't need to run validations here, since find_by_canonical_slug
+ # guarantees that there is no conflict in canonical_slug, and DB
+ # constraints on title and project_id/group_id enforce our other invariants
+ # This saves us a query.
+ meta
+ end
+ end
+
+ def find_by_canonical_slug(canonical_slug, container)
+ meta, conflict = with_canonical_slug(canonical_slug)
+ .where(container_attrs(container))
+ .limit(2)
+
+ if conflict.present?
+ meta.errors.add(:canonical_slug, 'Duplicate value found')
+ raise CanonicalSlugConflictError.new(meta)
+ end
+
+ meta
+ end
+
+ private
+
+ def wiki_page_updates(wiki_page)
+ last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc
+
+ {
+ title: wiki_page.title,
+ created_at: last_commit_date,
+ updated_at: last_commit_date
+ }
+ end
+
+ def container_key
+ raise NotImplementedError
+ end
+
+ def container_attrs(container)
+ { container_key => container.id }
+ end
+ end
+
+ def canonical_slug
+ strong_memoize(:canonical_slug) { slugs.canonical.take&.slug }
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def canonical_slug=(slug)
+ return if @canonical_slug == slug
+
+ if persisted?
+ transaction do
+ slugs.canonical.update_all(canonical: false)
+ page_slug = slugs.create_with(canonical: true).find_or_create_by(slug: slug)
+ page_slug.update_columns(canonical: true) unless page_slug.canonical?
+ end
+ else
+ slugs.new(slug: slug, canonical: true)
+ end
+
+ @canonical_slug = slug
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def update_state(created, known_slugs, wiki_page, updates)
+ update_wiki_page_attributes(updates)
+ insert_slugs(known_slugs, created, wiki_page.slug)
+ self.canonical_slug = wiki_page.slug
+ end
+
+ private
+
+ def update_wiki_page_attributes(updates)
+ # Remove all unnecessary updates:
+ updates.delete(:updated_at) if updated_at == updates[:updated_at]
+ updates.delete(:created_at) if created_at <= updates[:created_at]
+ updates.delete(:title) if title == updates[:title]
+
+ update_columns(updates) unless updates.empty?
+ end
+
+ def insert_slugs(strings, is_new, canonical_slug)
+ creation = Time.current.utc
+
+ slug_attrs = strings.map do |slug|
+ slug_attributes(slug, canonical_slug, is_new, creation)
+ end
+ slugs.insert_all(slug_attrs) unless !is_new && slug_attrs.size == 1
+
+ @canonical_slug = canonical_slug if is_new || strings.size == 1 # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ def slug_attributes(slug, canonical_slug, is_new, creation)
+ {
+ slug: slug,
+ canonical: (is_new && slug == canonical_slug),
+ created_at: creation,
+ updated_at: creation
+ }.merge(slug_meta_attributes)
+ end
+
+ def slug_meta_attributes
+ { self.association(:slugs).reflection.foreign_key => id }
+ end
+
+ def no_two_metarecords_in_same_container_can_have_same_canonical_slug
+ container_id = attributes[self.class.container_key.to_s]
+
+ return unless container_id.present? && canonical_slug.present?
+
+ offending = self.class.with_canonical_slug(canonical_slug).where(self.class.container_key => container_id)
+ offending = offending.where.not(id: id) if persisted?
+
+ if offending.exists?
+ errors.add(:canonical_slug, 'each page in a wiki must have a distinct canonical slug')
+ end
+ end
+end
diff --git a/app/models/concerns/has_wiki_page_slug_attributes.rb b/app/models/concerns/has_wiki_page_slug_attributes.rb
new file mode 100644
index 00000000000..3335eccbaf6
--- /dev/null
+++ b/app/models/concerns/has_wiki_page_slug_attributes.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module HasWikiPageSlugAttributes
+ extend ActiveSupport::Concern
+
+ included do
+ validates :slug, uniqueness: { scope: meta_foreign_key }
+ validates :slug, length: { maximum: 2048 }, allow_nil: false
+ validates :canonical, uniqueness: {
+ scope: meta_foreign_key,
+ if: :canonical?,
+ message: 'Only one slug can be canonical per wiki metadata record'
+ }
+
+ scope :canonical, -> { where(canonical: true) }
+
+ def update_columns(attrs = {})
+ super(attrs.reverse_merge(updated_at: Time.current.utc))
+ end
+ end
+
+ def self.update_all(attrs = {})
+ super(attrs.reverse_merge(updated_at: Time.current.utc))
+ end
+end
diff --git a/app/models/concerns/ignorable_columns.rb b/app/models/concerns/ignorable_columns.rb
index 744a1f0b5f3..4cbcb25406d 100644
--- a/app/models/concerns/ignorable_columns.rb
+++ b/app/models/concerns/ignorable_columns.rb
@@ -31,15 +31,13 @@ module IgnorableColumns
alias_method :ignore_column, :ignore_columns
def ignored_columns_details
- unless defined?(@ignored_columns_details)
- IGNORE_COLUMN_MUTEX.synchronize do
- @ignored_columns_details ||= superclass.try(:ignored_columns_details)&.dup || {}
- end
- end
+ return @ignored_columns_details if defined?(@ignored_columns_details)
- @ignored_columns_details
+ IGNORE_COLUMN_MONITOR.synchronize do
+ @ignored_columns_details ||= superclass.try(:ignored_columns_details)&.dup || {}
+ end
end
- IGNORE_COLUMN_MUTEX = Mutex.new
+ IGNORE_COLUMN_MONITOR = Monitor.new
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 7624a1a4e80..c3a394c1ca5 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -84,7 +84,6 @@ module Issuable
validate :description_max_length_for_new_records_is_valid, on: :update
before_validation :truncate_description_on_import!
- after_save :store_mentions!, if: :any_mentionable_attributes_changed?
scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
@@ -198,7 +197,7 @@ module Issuable
end
def severity
- return IssuableSeverity::DEFAULT unless incident?
+ return IssuableSeverity::DEFAULT unless supports_severity?
issuable_severity&.severity || IssuableSeverity::DEFAULT
end
@@ -305,14 +304,12 @@ module Issuable
end
def order_labels_priority(direction = 'ASC', excluded_labels: [], extra_select_columns: [], with_cte: false)
- params = {
+ highest_priority = highest_label_priority(
target_type: name,
target_column: "#{table_name}.id",
project_column: "#{table_name}.#{project_foreign_key}",
excluded_labels: excluded_labels
- }
-
- highest_priority = highest_label_priority(params).to_sql
+ ).to_sql
# When using CTE make sure to select the same columns that are on the group_by clause.
# This prevents errors when ignored columns are present in the database.
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index b10e8547e86..5db077c178d 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -80,37 +80,6 @@ module Mentionable
all_references(current_user).users
end
- def store_mentions!
- refs = all_references(self.author)
-
- references = {}
- references[:mentioned_users_ids] = refs.mentioned_users&.pluck(:id).presence
- references[:mentioned_groups_ids] = refs.mentioned_groups&.pluck(:id).presence
- references[:mentioned_projects_ids] = refs.mentioned_projects&.pluck(:id).presence
-
- # One retry should be enough as next time `model_user_mention` should return the existing mention record, that
- # threw the `ActiveRecord::RecordNotUnique` exception in first place.
- self.class.safe_ensure_unique(retries: 1) do
- user_mention = model_user_mention
-
- # this may happen due to notes polymorphism, so noteable_id may point to a record that no longer exists
- # as we cannot have FK on noteable_id
- break if user_mention.blank?
-
- user_mention.mentioned_users_ids = references[:mentioned_users_ids]
- user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
- user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
-
- if user_mention.has_mentions?
- user_mention.save!
- else
- user_mention.destroy!
- end
- end
-
- true
- end
-
def referenced_users
User.where(id: user_mentions.select("unnest(mentioned_users_ids)"))
end
@@ -216,12 +185,6 @@ module Mentionable
source.select { |key, val| mentionable.include?(key) }
end
- def any_mentionable_attributes_changed?
- self.class.mentionable_attrs.any? do |attr|
- saved_changes.key?(attr.first)
- end
- end
-
# Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target.
def cross_reference_exists?(target)
@@ -237,12 +200,12 @@ module Mentionable
end
# User mention that is parsed from model description rather then its related notes.
- # Models that have a descriprion attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
+ # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
# a description attribute.
#
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
- # in a multithreaded environment. Make sure to use it within a *safe_ensure_unique* block.
+ # in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block.
def model_user_mention
user_mentions.where(note_id: nil).first_or_initialize
end
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
index 7be4a26d4fa..82055822cfb 100644
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
module OptimizedIssuableLabelFilter
+ extend ActiveSupport::Concern
+
+ prepended do
+ extend Gitlab::Cache::RequestCache
+
+ # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
+ request_cache(:find_label_ids) { [root_namespace.id, params.label_names] }
+ end
+
def by_label(items)
return items unless params.labels?
@@ -41,7 +50,7 @@ module OptimizedIssuableLabelFilter
def issuables_with_selected_labels(items, target_model)
if root_namespace
- all_label_ids = find_label_ids(root_namespace)
+ all_label_ids = find_label_ids
# Found less labels in the DB than we were searching for. Return nothing.
return items.none if all_label_ids.size != params.label_names.size
@@ -57,18 +66,20 @@ module OptimizedIssuableLabelFilter
items
end
- def find_label_ids(root_namespace)
- finder_params = {
- include_subgroups: true,
- include_ancestor_groups: true,
- include_descendant_groups: true,
- group: root_namespace,
- title: params.label_names
- }
-
- LabelsFinder
- .new(nil, finder_params)
- .execute(skip_authorization: true)
+ def find_label_ids
+ group_labels = Label
+ .where(project_id: nil)
+ .where(title: params.label_names)
+ .where(group_id: root_namespace.self_and_descendants.select(:id))
+
+ project_labels = Label
+ .where(group_id: nil)
+ .where(title: params.label_names)
+ .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendants.select(:id)))
+
+ Label
+ .from_union([group_labels, project_labels], remove_duplicates: false)
+ .reorder(nil)
.pluck(:title, :id)
.group_by(&:first)
.values
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index b69fb2931c3..07bec07e556 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -70,6 +70,14 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:metrics_dashboard_access_level, value)
end
+ def analytics_access_level=(value)
+ write_feature_attribute_string(:analytics_access_level, value)
+ end
+
+ def operations_access_level=(value)
+ write_feature_attribute_string(:operations_access_level, value)
+ end
+
private
def write_feature_attribute_boolean(field, value)
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index cddca72f91f..65195a8d5aa 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -12,6 +12,10 @@ module ProtectedRef
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
scope :for_project, ->(project) { where(project: project) }
+
+ def allow_multiple?(type)
+ false
+ end
end
def commit
@@ -29,7 +33,7 @@ module ProtectedRef
# to fail.
has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
- validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
+ validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }, unless: -> { allow_multiple?(type) }
accepts_nested_attributes_for :"#{type}_access_levels", allow_destroy: true
end
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 28dc3366e51..5e38ce7cad8 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -45,6 +45,7 @@ module ProtectedRefAccess
end
def check_access(user)
+ return false unless user
return true if user.admin?
user.can?(:push_code, project) &&
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 3470bdab5fb..dbc70ac2218 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
# The usage of the ReactiveCaching module is documented here:
-# https://docs.gitlab.com/ee/development/reactive_caching.md
+# https://docs.gitlab.com/ee/development/reactive_caching.html
+#
module ReactiveCaching
extend ActiveSupport::Concern
diff --git a/app/models/concerns/repository_storage_movable.rb b/app/models/concerns/repository_storage_movable.rb
new file mode 100644
index 00000000000..a45b4626628
--- /dev/null
+++ b/app/models/concerns/repository_storage_movable.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+module RepositoryStorageMovable
+ extend ActiveSupport::Concern
+ include AfterCommitQueue
+
+ included do
+ scope :order_created_at_desc, -> { order(created_at: :desc) }
+
+ validates :container, presence: true
+ validates :state, presence: true
+ validates :source_storage_name,
+ on: :create,
+ presence: true,
+ inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
+ validates :destination_storage_name,
+ on: :create,
+ presence: true,
+ inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
+ validate :container_repository_writable, on: :create
+
+ default_value_for(:destination_storage_name, allows_nil: false) do
+ pick_repository_storage
+ end
+
+ state_machine initial: :initial do
+ event :schedule do
+ transition initial: :scheduled
+ end
+
+ event :start do
+ transition scheduled: :started
+ end
+
+ event :finish_replication do
+ transition started: :replicated
+ end
+
+ event :finish_cleanup do
+ transition replicated: :finished
+ end
+
+ event :do_fail do
+ transition [:initial, :scheduled, :started] => :failed
+ transition replicated: :cleanup_failed
+ end
+
+ around_transition initial: :scheduled do |storage_move, block|
+ block.call
+
+ begin
+ storage_move.container.set_repository_read_only!(skip_git_transfer_check: true)
+ rescue => err
+ storage_move.add_error(err.message)
+ next false
+ end
+
+ storage_move.run_after_commit do
+ storage_move.schedule_repository_storage_update_worker
+ end
+
+ true
+ end
+
+ before_transition started: :replicated do |storage_move|
+ storage_move.container.set_repository_writable!
+
+ storage_move.update_repository_storage(storage_move.destination_storage_name)
+ end
+
+ before_transition started: :failed do |storage_move|
+ storage_move.container.set_repository_writable!
+ end
+
+ state :initial, value: 1
+ state :scheduled, value: 2
+ state :started, value: 3
+ state :finished, value: 4
+ state :failed, value: 5
+ state :replicated, value: 6
+ state :cleanup_failed, value: 7
+ end
+ end
+
+ class_methods do
+ private
+
+ def pick_repository_storage
+ container_klass = reflect_on_association(:container).class_name.constantize
+
+ container_klass.pick_repository_storage
+ end
+ end
+
+ # Projects, snippets, and group wikis has different db structure. In projects,
+ # we need to update some columns in this step, but we don't with the other resources.
+ #
+ # Therefore, we create this No-op method for snippets and wikis and let project
+ # overwrite it in their implementation.
+ def update_repository_storage(new_storage)
+ # No-op
+ end
+
+ def schedule_repository_storage_update_worker
+ raise NotImplementedError
+ end
+
+ def add_error(message)
+ errors.add(error_key, message)
+ end
+
+ private
+
+ def container_repository_writable
+ add_error(_('is read only')) if container&.repository_read_only?
+ end
+
+ def error_key
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index c70ce9bebcc..71d8e06de76 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -4,6 +4,36 @@
# Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable
extend ActiveSupport::Concern
+ include CaseSensitivity
+
+ # Finds a Routable object by its full path, without knowing the class.
+ #
+ # Usage:
+ #
+ # Routable.find_by_full_path('groupname') # -> Group
+ # Routable.find_by_full_path('groupname/projectname') # -> Project
+ #
+ # Returns a single object, or nil.
+ def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute)
+ return unless path.present?
+
+ # Case sensitive match first (it's cheaper and the usual case)
+ # If we didn't have an exact match, we perform a case insensitive search
+ #
+ # We need to qualify the columns with the table name, to support both direct lookups on
+ # Route/RedirectRoute, and scoped lookups through the Routable classes.
+ route =
+ route_scope.find_by(routes: { path: path }) ||
+ route_scope.iwhere(Route.arel_table[:path] => path).take
+
+ if follow_redirects
+ route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
+ end
+
+ return unless route
+
+ route.is_a?(Routable) ? route : route.source
+ end
included do
# Remove `inverse_of: source` when upgraded to rails 5.2
@@ -30,15 +60,14 @@ module Routable
#
# Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false)
- # Case sensitive match first (it's cheaper and the usual case)
- # If we didn't have an exact match, we perform a case insensitive search
- found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take
-
- return found if found
-
- if follow_redirects
- joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path)
- end
+ # TODO: Optimize these queries by avoiding joins
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/292252
+ Routable.find_by_full_path(
+ path,
+ follow_redirects: follow_redirects,
+ route_scope: includes(:route).references(:routes),
+ redirect_route_scope: joins(:redirect_routes)
+ )
end
# Builds a relation to find multiple objects by their full paths.
diff --git a/app/models/concerns/shardable.rb b/app/models/concerns/shardable.rb
index c0883c08289..4bebb99d195 100644
--- a/app/models/concerns/shardable.rb
+++ b/app/models/concerns/shardable.rb
@@ -8,6 +8,7 @@ module Shardable
scope :for_repository_storage, -> (repository_storage) { joins(:shard).where(shards: { name: repository_storage }) }
scope :excluding_repository_storage, -> (repository_storage) { joins(:shard).where.not(shards: { name: repository_storage }) }
+ scope :for_shard, -> (shard) { where(shard_id: shard) }
validates :shard, presence: true
end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index 23fd73f2904..8273059b30c 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -12,10 +12,16 @@ module Timebox
include FromUnion
TimeboxStruct = Struct.new(:title, :name, :id) do
+ include GlobalID::Identification
+
# Ensure these models match the interface required for exporting
def serializable_hash(_opts = {})
{ title: title, name: name, id: id }
end
+
+ def self.declarative_policy_class
+ "TimeboxPolicy"
+ end
end
# Represents a "No Timebox" state used for filtering Issues and Merge
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index a1f83884f02..535cf25eb9d 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -57,6 +57,13 @@ module TokenAuthenticatable
token = read_attribute(token_field)
token.present? && ActiveSupport::SecurityUtils.secure_compare(other_token, token)
end
+
+ # Base strategy delegates to this method for formatting a token before
+ # calling set_token. Can be overridden in models to e.g. add a prefix
+ # to the tokens
+ mod.define_method("format_#{token_field}") do |token|
+ token
+ end
end
def token_authenticatable_module
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index aafd0b538a3..f72a41f06b1 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -18,10 +18,15 @@ module TokenAuthenticatableStrategies
raise NotImplementedError
end
- def set_token(instance)
+ def set_token(instance, token)
raise NotImplementedError
end
+ # Default implementation returns the token as-is
+ def format_token(instance, token)
+ instance.send("format_#{@token_field}", token) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def ensure_token(instance)
write_new_token(instance) unless token_set?(instance)
get_token(instance)
@@ -57,7 +62,8 @@ module TokenAuthenticatableStrategies
def write_new_token(instance)
new_token = generate_available_token
- set_token(instance, new_token)
+ formatted_token = format_token(instance, new_token)
+ set_token(instance, formatted_token)
end
def unique
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index 325a5531926..473b430bb04 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -15,14 +15,13 @@ module TriggerableHooks
wiki_page_hooks: :wiki_page_events,
deployment_hooks: :deployment_events,
feature_flag_hooks: :feature_flag_events,
- release_hooks: :releases_events
+ release_hooks: :releases_events,
+ member_hooks: :member_events
}.freeze
extend ActiveSupport::Concern
class_methods do
- attr_reader :triggerable_hooks
-
attr_reader :triggers
def hooks_for(trigger)