diff options
Diffstat (limited to 'lib/gitlab/import_export/base_relation_factory.rb')
-rw-r--r-- | lib/gitlab/import_export/base_relation_factory.rb | 306 |
1 files changed, 0 insertions, 306 deletions
diff --git a/lib/gitlab/import_export/base_relation_factory.rb b/lib/gitlab/import_export/base_relation_factory.rb deleted file mode 100644 index fcb516fb3a1..00000000000 --- a/lib/gitlab/import_export/base_relation_factory.rb +++ /dev/null @@ -1,306 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module ImportExport - class BaseRelationFactory - include Gitlab::Utils::StrongMemoize - - IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - - OVERRIDES = {}.freeze - EXISTING_OBJECT_RELATIONS = %i[].freeze - - # This represents all relations that have unique key on `project_id` or `group_id` - UNIQUE_RELATIONS = %i[].freeze - - USER_REFERENCES = %w[ - author_id - assignee_id - updated_by_id - merged_by_id - latest_closed_by_id - user_id - created_by_id - last_edited_by_id - merge_user_id - resolved_by_id - closed_by_id - owner_id - ].freeze - - TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze - - def self.create(*args) - new(*args).create - end - - def self.relation_class(relation_name) - # There are scenarios where the model is pluralized (e.g. - # MergeRequest::Metrics), and we don't want to force it to singular - # with #classify. - relation_name.to_s.classify.constantize - rescue NameError - relation_name.to_s.constantize - end - - def initialize(relation_sym:, relation_hash:, members_mapper:, object_builder:, user:, importable:, excluded_keys: []) - @relation_name = self.class.overrides[relation_sym]&.to_sym || relation_sym - @relation_hash = relation_hash.except('noteable_id') - @members_mapper = members_mapper - @object_builder = object_builder - @user = user - @importable = importable - @imported_object_retries = 0 - @relation_hash[importable_column_name] = @importable.id - - # Remove excluded keys from relation_hash - # We don't do this in the parsed_relation_hash because of the 'transformed attributes' - # For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then, - # in the create method that attribute is renamed to diff. And because diff is an excluded key, - # if we clean the excluded keys in the parsed_relation_hash, it will be removed - # from the object attributes and the export will fail. - @relation_hash.except!(*excluded_keys) - end - - # Creates an object from an actual model with name "relation_sym" with params from - # the relation_hash, updating references with new object IDs, mapping users using - # the "members_mapper" object, also updating notes if required. - def create - return if invalid_relation? - - setup_base_models - setup_models - - generate_imported_object - end - - def self.overrides - self::OVERRIDES - end - - def self.existing_object_relations - self::EXISTING_OBJECT_RELATIONS - end - - private - - def invalid_relation? - false - end - - def setup_models - raise NotImplementedError - end - - def unique_relations - # define in sub-class if any - self.class::UNIQUE_RELATIONS - end - - def setup_base_models - update_user_references - remove_duplicate_assignees - reset_tokens! - remove_encrypted_attributes! - end - - def update_user_references - self.class::USER_REFERENCES.each do |reference| - if @relation_hash[reference] - @relation_hash[reference] = @members_mapper.map[@relation_hash[reference]] - end - end - end - - def remove_duplicate_assignees - return unless @relation_hash['issue_assignees'] - - # When an assignee did not exist in the members mapper, the importer is - # assigned. We only need to assign each user once. - @relation_hash['issue_assignees'].uniq!(&:user_id) - end - - def generate_imported_object - imported_object - end - - def reset_tokens! - return unless Gitlab::ImportExport.reset_tokens? && self.class::TOKEN_RESET_MODELS.include?(@relation_name) - - # If we import/export to the same instance, tokens will have to be reset. - # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across. - relation_class.attribute_names.select { |name| name.include?('token') }.each do |token| - @relation_hash[token] = nil - end - end - - def remove_encrypted_attributes! - return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any? - - relation_class.encrypted_attributes.each_key do |key| - @relation_hash[key.to_s] = nil - end - end - - def relation_class - @relation_class ||= self.class.relation_class(@relation_name) - end - - def importable_column_name - importable_class_name.concat('_id') - end - - def importable_class_name - @importable.class.to_s.downcase - end - - def imported_object - if existing_or_new_object.respond_to?(:importing) - existing_or_new_object.importing = true - end - - existing_or_new_object - rescue ActiveRecord::RecordNotUnique - # as the operation is not atomic, retry in the unlikely scenario an INSERT is - # performed on the same object between the SELECT and the INSERT - @imported_object_retries += 1 - retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES - end - - def parsed_relation_hash - @parsed_relation_hash ||= Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: @relation_hash, - relation_class: relation_class) - end - - def existing_or_new_object - # Only find existing records to avoid mapping tables such as milestones - # Otherwise always create the record, skipping the extra SELECT clause. - @existing_or_new_object ||= begin - if existing_object? - attribute_hash = attribute_hash_for(['events']) - - existing_object.assign_attributes(attribute_hash) if attribute_hash.any? - - existing_object - else - # Because of single-type inheritance, we need to be careful to use the `type` field - # See https://gitlab.com/gitlab-org/gitlab/issues/34860#note_235321497 - inheritance_column = relation_class.try(:inheritance_column) - inheritance_attributes = parsed_relation_hash.slice(inheritance_column) - object = relation_class.new(inheritance_attributes) - object.assign_attributes(parsed_relation_hash) - object - end - end - end - - def attribute_hash_for(attributes) - attributes.each_with_object({}) do |hash, value| - hash[value] = parsed_relation_hash.delete(value) if parsed_relation_hash[value] - hash - end - end - - def existing_object - @existing_object ||= find_or_create_object! - end - - def unique_relation_object - unique_relation_object = relation_class.find_or_create_by(importable_column_name => @importable.id) - unique_relation_object.assign_attributes(parsed_relation_hash) - unique_relation_object - end - - def find_or_create_object! - return unique_relation_object if unique_relation? - - # Can't use IDs as validation exists calling `group` or `project` attributes - finder_hash = parsed_relation_hash.tap do |hash| - if relation_class.attribute_method?('group_id') && @importable.is_a?(Project) - hash['group'] = @importable.group - end - - hash[importable_class_name] = @importable if relation_class.reflect_on_association(importable_class_name.to_sym) - hash.delete(importable_column_name) - end - - @object_builder.build(relation_class, finder_hash) - end - - def setup_note - set_note_author - # attachment is deprecated and note uploads are handled by Markdown uploader - @relation_hash['attachment'] = nil - end - - # Sets the author for a note. If the user importing the project - # has admin access, an actual mapping with new project members - # will be used. Otherwise, a note stating the original author name - # is left. - def set_note_author - old_author_id = @relation_hash['author_id'] - author = @relation_hash.delete('author') - - update_note_for_missing_author(author['name']) unless has_author?(old_author_id) - end - - def has_author?(old_author_id) - admin_user? && @members_mapper.include?(old_author_id) - end - - def missing_author_note(updated_at, author_name) - timestamp = updated_at.split('.').first - "\n\n *By #{author_name} on #{timestamp} (imported from GitLab project)*" - end - - def update_note_for_missing_author(author_name) - @relation_hash['note'] = '*Blank note*' if @relation_hash['note'].blank? - @relation_hash['note'] = "#{@relation_hash['note']}#{missing_author_note(@relation_hash['updated_at'], author_name)}" - end - - def admin_user? - @user.admin? - end - - def existing_object? - strong_memoize(:_existing_object) do - self.class.existing_object_relations.include?(@relation_name) || unique_relation? - end - end - - def unique_relation? - strong_memoize(:unique_relation) do - importable_foreign_key.present? && - (has_unique_index_on_importable_fk? || uses_importable_fk_as_primary_key?) - end - end - - def has_unique_index_on_importable_fk? - cache = cached_has_unique_index_on_importable_fk - table_name = relation_class.table_name - return cache[table_name] if cache.has_key?(table_name) - - index_exists = - ActiveRecord::Base.connection.index_exists?( - relation_class.table_name, - importable_foreign_key, - unique: true) - - cache[table_name] = index_exists - end - - # Avoid unnecessary DB requests - def cached_has_unique_index_on_importable_fk - Thread.current[:cached_has_unique_index_on_importable_fk] ||= {} - end - - def uses_importable_fk_as_primary_key? - relation_class.primary_key == importable_foreign_key - end - - def importable_foreign_key - relation_class.reflect_on_association(importable_class_name.to_sym)&.foreign_key - end - end - end -end |