Commit ae8ad17f authored by Stan Hu's avatar Stan Hu

Merge branch 'kamil-refactor-import-structure' into 'master'

Normalize import_export structure

See merge request gitlab-org/gitlab-ce!32704
parents 20c7c123 0eeadb2d
...@@ -3,35 +3,19 @@ ...@@ -3,35 +3,19 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
class AttributesFinder class AttributesFinder
def initialize(included_attributes:, excluded_attributes:, methods:) def initialize(config:)
@included_attributes = included_attributes || {} @tree = config[:tree] || {}
@excluded_attributes = excluded_attributes || {} @included_attributes = config[:included_attributes] || {}
@methods = methods || {} @excluded_attributes = config[:excluded_attributes] || {}
@methods = config[:methods] || {}
end end
def find(model_object) def find_root(model_key)
parsed_hash = find_attributes_only(model_object) find(model_key, @tree[model_key])
parsed_hash.empty? ? model_object : { model_object => parsed_hash }
end end
def parse(model_object) def find_relations_tree(model_key)
parsed_hash = find_attributes_only(model_object) @tree[model_key]
yield parsed_hash unless parsed_hash.empty?
end
def find_included(value)
key = key_from_hash(value)
@included_attributes[key].nil? ? {} : { only: @included_attributes[key] }
end
def find_excluded(value)
key = key_from_hash(value)
@excluded_attributes[key].nil? ? {} : { except: @excluded_attributes[key] }
end
def find_method(value)
key = key_from_hash(value)
@methods[key].nil? ? {} : { methods: @methods[key] }
end end
def find_excluded_keys(klass_name) def find_excluded_keys(klass_name)
...@@ -40,12 +24,24 @@ module Gitlab ...@@ -40,12 +24,24 @@ module Gitlab
private private
def find_attributes_only(value) def find(model_key, model_tree)
find_included(value).merge(find_excluded(value)).merge(find_method(value)) {
only: @included_attributes[model_key],
except: @excluded_attributes[model_key],
methods: @methods[model_key],
include: resolve_model_tree(model_tree)
}.compact
end
def resolve_model_tree(model_tree)
return unless model_tree
model_tree
.map(&method(:resolve_model))
end end
def key_from_hash(value) def resolve_model(model_key, model_tree)
value.is_a?(Hash) ? value.first.first : value { model_key => find(model_key, model_tree) }
end end
end end
end end
......
...@@ -3,70 +3,49 @@ ...@@ -3,70 +3,49 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
class Config class Config
def initialize
@hash = parse_yaml
@hash.deep_symbolize_keys!
@ee_hash = @hash.delete(:ee) || {}
@hash[:tree] = normalize_tree(@hash[:tree])
@ee_hash[:tree] = normalize_tree(@ee_hash[:tree] || {})
end
# Returns a Hash of the YAML file, including EE specific data if EE is # Returns a Hash of the YAML file, including EE specific data if EE is
# used. # used.
def to_h def to_h
hash = parse_yaml if merge_ee?
ee_hash = hash['ee'] deep_merge(@hash, @ee_hash)
else
if merge? && ee_hash @hash
ee_hash.each do |key, value|
if key == 'project_tree'
merge_project_tree(value, hash[key])
else
merge_attributes_list(value, hash[key])
end
end
end end
# We don't want to expose this section after this point, as it is no
# longer needed.
hash.delete('ee')
hash
end end
# Merges a project relationships tree into the target tree. private
#
# @param [Array<Hash|Symbol>] source_values
# @param [Array<Hash|Symbol>] target_values
def merge_project_tree(source_values, target_values)
source_values.each do |value|
if value.is_a?(Hash)
# Examples:
#
# { 'project_tree' => [{ 'labels' => [...] }] }
# { 'notes' => [:author, { 'events' => [:push_event_payload] }] }
value.each do |key, val|
target = target_values
.find { |h| h.is_a?(Hash) && h[key] }
if target def deep_merge(hash_a, hash_b)
merge_project_tree(val, target[key]) hash_a.deep_merge(hash_b) do |_, this_val, other_val|
else this_val.to_a + other_val.to_a
target_values << { key => val.dup }
end
end
else
# Example: :priorities, :author, etc
target_values << value
end
end end
end end
# Merges a Hash containing a flat list of attributes, such as the entries def normalize_tree(item)
# in a `excluded_attributes` section. case item
# when Array
# @param [Hash] source_values item.reduce({}) do |hash, subitem|
# @param [Hash] target_values hash.merge!(normalize_tree(subitem))
def merge_attributes_list(source_values, target_values) end
source_values.each do |key, values| when Hash
target_values[key] ||= [] item.transform_values(&method(:normalize_tree))
target_values[key].concat(values) when Symbol
{ item => {} }
else
raise ArgumentError, "#{item} needs to be Array, Hash, Symbol or NilClass"
end end
end end
def merge? def merge_ee?
Gitlab.ee? Gitlab.ee?
end end
......
...@@ -3,89 +3,92 @@ ...@@ -3,89 +3,92 @@
# This list _must_ only contain relationships that are available to both CE and # This list _must_ only contain relationships that are available to both CE and
# EE. EE specific relationships must be defined in the `ee` section further # EE. EE specific relationships must be defined in the `ee` section further
# down below. # down below.
project_tree: tree:
- labels: project:
- :priorities - labels:
- milestones: - :priorities
- events: - milestones:
- :push_event_payload
- issues:
- events:
- :push_event_payload
- :timelogs
- notes:
- :author
- events:
- :push_event_payload
- label_links:
- label:
- :priorities
- milestone:
- events:
- :push_event_payload
- resource_label_events:
- label:
- :priorities
- :issue_assignees
- snippets:
- :award_emoji
- notes:
- :author
- releases:
- :links
- project_members:
- :user
- merge_requests:
- :metrics
- notes:
- :author
- events: - events:
- :push_event_payload - :push_event_payload
- :suggestions - issues:
- merge_request_diff:
- :merge_request_diff_commits
- :merge_request_diff_files
- events:
- :push_event_payload
- :timelogs
- label_links:
- label:
- :priorities
- milestone:
- events: - events:
- :push_event_payload - :push_event_payload
- resource_label_events: - :timelogs
- label: - notes:
- :priorities - :author
- ci_pipelines: - events:
- notes: - :push_event_payload
- :author - label_links:
- label:
- :priorities
- milestone:
- events:
- :push_event_payload
- resource_label_events:
- label:
- :priorities
- :issue_assignees
- snippets:
- :award_emoji
- notes:
- :author
- releases:
- :links
- project_members:
- :user
- merge_requests:
- :metrics
- notes:
- :author
- events:
- :push_event_payload
- :suggestions
- merge_request_diff:
- :merge_request_diff_commits
- :merge_request_diff_files
- events: - events:
- :push_event_payload - :push_event_payload
- stages: - :timelogs
- :statuses - label_links:
- :external_pull_request - label:
- :external_pull_requests - :priorities
- :auto_devops - milestone:
- :triggers - events:
- :pipeline_schedules - :push_event_payload
- :services - resource_label_events:
- protected_branches: - label:
- :merge_access_levels - :priorities
- :push_access_levels - ci_pipelines:
- protected_tags: - notes:
- :create_access_levels - :author
- :project_feature - events:
- :custom_attributes - :push_event_payload
- :prometheus_metrics - stages:
- :project_badges - :statuses
- :ci_cd_settings - :external_pull_request
- :error_tracking_setting - :external_pull_requests
- :metrics_setting - :auto_devops
- boards: - :triggers
- lists: - :pipeline_schedules
- label: - :services
- :priorities - protected_branches:
- :merge_access_levels
- :push_access_levels
- protected_tags:
- :create_access_levels
- :project_feature
- :custom_attributes
- :prometheus_metrics
- :project_badges
- :ci_cd_settings
- :error_tracking_setting
- :metrics_setting
- boards:
- lists:
- label:
- :priorities
group_members:
- :user
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
included_attributes: included_attributes:
...@@ -225,12 +228,15 @@ methods: ...@@ -225,12 +228,15 @@ methods:
- :type - :type
lists: lists:
- :list_type - :list_type
ci_pipelines:
- :notes
# EE specific relationships and settings to include. All of this will be merged # EE specific relationships and settings to include. All of this will be merged
# into the previous structures if EE is used. # into the previous structures if EE is used.
ee: ee:
project_tree: tree:
- protected_branches: project:
- :unprotect_access_levels protected_branches:
- protected_environments: - :unprotect_access_levels
- :deploy_access_levels protected_environments:
- :deploy_access_levels
# frozen_string_literal: true
module Gitlab
module ImportExport
# Generates a hash that conforms with http://apidock.com/rails/Hash/to_json
# and its peculiar options.
class JsonHashBuilder
def self.build(model_objects, attributes_finder)
new(model_objects, attributes_finder).build
end
def initialize(model_objects, attributes_finder)
@model_objects = model_objects
@attributes_finder = attributes_finder
end
def build
process_model_objects(@model_objects)
end
private
# Called when the model is actually a hash containing other relations (more models)
# Returns the config in the right format for calling +to_json+
#
# +model_object_hash+ - A model relationship such as:
# {:merge_requests=>[:merge_request_diff, :notes]}
def process_model_objects(model_object_hash)
json_config_hash = {}
current_key = model_object_hash.first.first
model_object_hash.values.flatten.each do |model_object|
@attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash }
handle_model_object(current_key, model_object, json_config_hash)
end
json_config_hash
end
# Creates or adds to an existing hash an individual model or list
#
# +current_key+ main model that will be a key in the hash
# +model_object+ model or list of models to include in the hash
# +json_config_hash+ the original hash containing the root model
def handle_model_object(current_key, model_object, json_config_hash)
model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object
if json_config_hash[current_key]
add_model_value(current_key, model_or_sub_model, json_config_hash)
else
create_model_value(current_key, model_or_sub_model, json_config_hash)
end
end
# Constructs a new hash that will hold the configuration for that particular object
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
#
# +current_key+ main model that will be a key in the hash
# +value+ existing model to be included in the hash
# +json_config_hash+ the original hash containing the root model
def create_model_value(current_key, value, json_config_hash)
json_config_hash[current_key] = parse_hash(value) || { include: value }
end
# Calls attributes finder to parse the hash and add any attributes to it
#
# +value+ existing model to be included in the hash
# +parsed_hash+ the original hash
def parse_hash(value)
return if already_contains_methods?(value)
@attributes_finder.parse(value) do |hash|
{ include: hash_or_merge(value, hash) }
end
end
def already_contains_methods?(value)
value.is_a?(Hash) && value.values.detect { |val| val[:methods]}
end
# Adds new model configuration to an existing hash with key +current_key+
# It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
#
# +current_key+ main model that will be a key in the hash
# +value+ existing model to be included in the hash
# +json_config_hash+ the original hash containing the root model
def add_model_value(current_key, value, json_config_hash)
@attributes_finder.parse(value) do |hash|
value = { value => hash } unless value.is_a?(Hash)
end
add_to_array(current_key, json_config_hash, value)
end
# Adds new model configuration to an existing hash with key +current_key+
# it creates a new array if it was previously a single value
#
# +current_key+ main model that will be a key in the hash
# +value+ existing model to be included in the hash
# +json_config_hash+ the original hash containing the root model
def add_to_array(current_key, json_config_hash, value)
old_values = json_config_hash[current_key][:include]
json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
end
# Construct a new hash or merge with an existing one a model configuration
# This is to fulfil +to_json+ requirements.
#
# +hash+ hash containing configuration generated mainly from +@attributes_finder+
# +value+ existing model to be included in the hash
def hash_or_merge(value, hash)
value.is_a?(Hash) ? value.merge(hash) : { value => hash }
end
end
end
end
...@@ -58,11 +58,13 @@ module Gitlab ...@@ -58,11 +58,13 @@ module Gitlab
# the configuration yaml file too. # the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project. # Finally, it updates each attribute in the newly imported project.
def create_relations def create_relations
default_relation_list.each do |relation| project_relations_without_project_members.each do |relation_key, relation_definition|
if relation.is_a?(Hash) relation_key_s = relation_key.to_s
create_sub_relations(relation, @tree_hash)
elsif @tree_hash[relation.to_s].present? if relation_definition.present?
save_relation_hash(@tree_hash[relation.to_s], relation) create_sub_relations(relation_key_s, relation_definition, @tree_hash)
elsif @tree_hash[relation_key_s].present?
save_relation_hash(relation_key_s, @tree_hash[relation_key_s])
end end
end end
...@@ -71,7 +73,7 @@ module Gitlab ...@@ -71,7 +73,7 @@ module Gitlab
@saved @saved
end end
def save_relation_hash(relation_hash_batch, relation_key) def save_relation_hash(relation_key, relation_hash_batch)
relation_hash = create_relation(relation_key, relation_hash_batch) relation_hash = create_relation(relation_key, relation_hash_batch)
remove_group_models(relation_hash) if relation_hash.is_a?(Array) remove_group_models(relation_hash) if relation_hash.is_a?(Array)
...@@ -91,10 +93,13 @@ module Gitlab ...@@ -91,10 +93,13 @@ module Gitlab
end end
end end
def default_relation_list def project_relations_without_project_members
reader.tree.reject do |model| # We remove `project_members` as they are deserialized separately
model.is_a?(Hash) && model[:project_members] project_relations.except(:project_members)
end end
def project_relations
reader.attributes_finder.find_relations_tree(:project)
end end
def restore_project def restore_project
...@@ -150,8 +155,7 @@ module Gitlab ...@@ -150,8 +155,7 @@ module Gitlab
# issue, finds any subrelations such as notes, creates them and assign them back to the hash # issue, finds any subrelations such as notes, creates them and assign them back to the hash
# #
# Recursively calls this method if the sub-relation is a hash containing more sub-relations # Recursively calls this method if the sub-relation is a hash containing more sub-relations
def create_sub_relations(relation, tree_hash, save: true) def create_sub_relations(relation_key, relation_definition, tree_hash, save: true)
relation_key = relation.keys.first.to_s
return if tree_hash[relation_key].blank? return if tree_hash[relation_key].blank?
tree_array = [tree_hash[relation_key]].flatten tree_array = [tree_hash[relation_key]].flatten
...@@ -171,13 +175,13 @@ module Gitlab ...@@ -171,13 +175,13 @@ module Gitlab
# But we can't have it in the upper level or GC won't get rid of the AR objects # But we can't have it in the upper level or GC won't get rid of the AR objects
# after we save the batch. # after we save the batch.
Project.transaction do Project.transaction do
process_sub_relation(relation, relation_item) process_sub_relation(relation_key, relation_definition, relation_item)
# For every subrelation that hangs from Project, save the associated records altogether # For every subrelation that hangs from Project, save the associated records altogether
# This effectively batches all records per subrelation item, only keeping those in memory # This effectively batches all records per subrelation item, only keeping those in memory
# We have to keep in mind that more batch granularity << Memory, but >> Slowness # We have to keep in mind that more batch granularity << Memory, but >> Slowness
if save if save
save_relation_hash([relation_item], relation_key) save_relation_hash(relation_key, [relation_item])
tree_hash[relation_key].delete(relation_item) tree_hash[relation_key].delete(relation_item)
end end
end end
...@@ -186,37 +190,35 @@ module Gitlab ...@@ -186,37 +190,35 @@ module Gitlab
tree_hash.delete(relation_key) if save tree_hash.delete(relation_key) if save
end end
def process_sub_relation(relation, relation_item) def process_sub_relation(relation_key, relation_definition, relation_item)
relation.values.flatten.each do |sub_relation| relation_definition.each do |sub_relation_key, sub_relation_definition|
# We just use author to get the user ID, do not attempt to create an instance. # We just use author to get the user ID, do not attempt to create an instance.
next if sub_relation == :author next if sub_relation_key == :author
create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash) sub_relation_key_s = sub_relation_key.to_s
relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation) # create dependent relations if present
relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank? if sub_relation_definition.present?
end create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false)
end end
def assign_relation_hash(relation_item, sub_relation) # transform relation hash to actual object
if sub_relation.is_a?(Hash) sub_relation_hash = relation_item[sub_relation_key_s]
relation_hash = relation_item[sub_relation.keys.first.to_s] if sub_relation_hash.present?
sub_relation = sub_relation.keys.first relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash)
else end
relation_hash = relation_item[sub_relation.to_s]
end end
[relation_hash, sub_relation]
end end
def create_relation(relation, relation_hash_list) def create_relation(relation_key, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash| relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym, Gitlab::ImportExport::RelationFactory.create(
relation_hash: relation_hash, relation_sym: relation_key.to_sym,
members_mapper: members_mapper, relation_hash: relation_hash,
user: @user, members_mapper: members_mapper,
project: @restored_project, user: @user,
excluded_keys: excluded_keys_for_relation(relation)) project: @restored_project,
excluded_keys: excluded_keys_for_relation(relation_key))
end.compact end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
......
...@@ -18,7 +18,10 @@ module Gitlab ...@@ -18,7 +18,10 @@ module Gitlab
def save def save
mkdir_p(@shared.export_path) mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree) project_tree = serialize_project_tree
fix_project_tree(project_tree)
File.write(full_path, project_tree.to_json)
true true
rescue => e rescue => e
@shared.error(e) @shared.error(e)
...@@ -27,27 +30,25 @@ module Gitlab ...@@ -27,27 +30,25 @@ module Gitlab
private private
def project_json_tree def fix_project_tree(project_tree)
if @params[:description].present? if @params[:description].present?
project_json['description'] = @params[:description] project_tree['description'] = @params[:description]
end end
project_json['project_members'] += group_members_json project_tree['project_members'] += group_members_array
RelationRenameService.add_new_associations(project_json)
project_json.to_json RelationRenameService.add_new_associations(project_tree)
end end
def project_json def serialize_project_tree
@project_json ||= @project.as_json(reader.project_tree) @project.as_json(reader.project_tree)
end end
def reader def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared) @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end end
def group_members_json def group_members_array
group_members.as_json(reader.group_members_tree).each do |group_member| group_members.as_json(reader.group_members_tree).each do |group_member|
group_member['source_type'] = 'Project' # Make group members project members of the future import group_member['source_type'] = 'Project' # Make group members project members of the future import
end end
......
...@@ -7,42 +7,22 @@ module Gitlab ...@@ -7,42 +7,22 @@ module Gitlab
def initialize(shared:) def initialize(shared:)
@shared = shared @shared = shared
config_hash = ImportExport::Config.new.to_h.deep_symbolize_keys
@tree = config_hash[:project_tree] @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(
@attributes_finder = Gitlab::ImportExport::AttributesFinder.new(included_attributes: config_hash[:included_attributes], config: ImportExport::Config.new.to_h)
excluded_attributes: config_hash[:excluded_attributes],
methods: config_hash[:methods])
end end
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html # Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations. # for outputting a project in JSON format, including its relations and sub relations.
def project_tree def project_tree
attributes = @attributes_finder.find(:project) attributes_finder.find_root(:project)
project_attributes = attributes.is_a?(Hash) ? attributes[:project] : {}
project_attributes.merge(include: build_hash(@tree))
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
end end
def group_members_tree def group_members_tree
@attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user)) attributes_finder.find_root(:group_members)
end
private
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
#
# +model_list+ - List of models as a relation tree to be included in the generated JSON, from the _import_export.yml_ file
def build_hash(model_list)
model_list.map do |model_objects|
if model_objects.is_a?(Hash)
Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder)
else
@attributes_finder.find(model_objects)
end
end
end end
end end
end end
......
...@@ -12,7 +12,7 @@ describe 'Import/Export attribute configuration' do ...@@ -12,7 +12,7 @@ describe 'Import/Export attribute configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys } let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:relation_names) do let(:relation_names) do
names = names_from_tree(config_hash['project_tree']) names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models # Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually. # - project is not part of the tree, so it has to be added manually.
......
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::ImportExport::AttributesFinder do
describe '#find_root' do
subject { described_class.new(config: config).find_root(model_key) }
let(:test_config) { 'spec/support/import_export/import_export.yml' }
let(:config) { Gitlab::ImportExport::Config.new.to_h }
let(:model_key) { :project }
let(:project_tree_hash) do
{
except: [:id, :created_at],
include: [
{ issues: { include: [] } },
{ labels: { include: [] } },
{ merge_requests: {
except: [:iid],
include: [
{ merge_request_diff: {
include: []
} },
{ merge_request_test: { include: [] } }
],
only: [:id]
} },
{ commit_statuses: {
include: [{ commit: { include: [] } }]
} },
{ project_members: {
include: [{ user: { include: [],
only: [:email] } }]
} }
]
}
end
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
end
it 'generates hash from project tree config' do
is_expected.to match(project_tree_hash)
end
context 'individual scenarios' do
it 'generates the correct hash for a single project relation' do
setup_yaml(tree: { project: [:issues] })
is_expected.to match(
include: [{ issues: { include: [] } }]
)
end
it 'generates the correct hash for a single project feature relation' do
setup_yaml(tree: { project: [:project_feature] })
is_expected.to match(
include: [{ project_feature: { include: [] } }]
)
end
it 'generates the correct hash for a multiple project relation' do
setup_yaml(tree: { project: [:issues, :snippets] })
is_expected.to match(
include: [{ issues: { include: [] } },
{ snippets: { include: [] } }]
)
end
it 'generates the correct hash for a single sub-relation' do
setup_yaml(tree: { project: [issues: [:notes]] })
is_expected.to match(
include: [{ issues: { include: [{ notes: { include: [] } }] } }]
)
end
it 'generates the correct hash for a multiple sub-relation' do
setup_yaml(tree: { project: [merge_requests: [:notes, :merge_request_diff]] })
is_expected.to match(
include: [{ merge_requests:
{ include: [{ notes: { include: [] } },
{ merge_request_diff: { include: [] } }] } }]
)
end
it 'generates the correct hash for a sub-relation with another sub-relation' do
setup_yaml(tree: { project: [merge_requests: [notes: [:author]]] })
is_expected.to match(
include: [{ merge_requests: {
include: [{ notes: { include: [{ author: { include: [] } }] } }]
} }]
)
end
it 'generates the correct hash for a relation with included attributes' do
setup_yaml(tree: { project: [:issues] },
included_attributes: { issues: [:name, :description] })
is_expected.to match(
include: [{ issues: { include: [],
only: [:name, :description] } }]
)
end
it 'generates the correct hash for a relation with excluded attributes' do
setup_yaml(tree: { project: [:issues] },
excluded_attributes: { issues: [:name] })
is_expected.to match(
include: [{ issues: { except: [:name],
include: [] } }]
)
end
it 'generates the correct hash for a relation with both excluded and included attributes' do
setup_yaml(tree: { project: [:issues] },
excluded_attributes: { issues: [:name] },
included_attributes: { issues: [:description] })
is_expected.to match(
include: [{ issues: { except: [:name],
include: [],
only: [:description] } }]
)
end
it 'generates the correct hash for a relation with custom methods' do
setup_yaml(tree: { project: [:issues] },
methods: { issues: [:name] })
is_expected.to match(
include: [{ issues: { include: [],
methods: [:name] } }]
)
end
def setup_yaml(hash)
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
end
end
end
describe '#find_relations_tree' do
subject { described_class.new(config: config).find_relations_tree(model_key) }
let(:tree) { { project: { issues: {} } } }
let(:model_key) { :project }
context 'when initialized with config including tree' do
let(:config) { { tree: tree } }
context 'when relation is in top-level keys of the tree' do
it { is_expected.to eq({ issues: {} }) }
end
context 'when the relation is not in top-level keys' do
let(:model_key) { :issues }
it { is_expected.to be_nil }
end
end
context 'when tree is not present in config' do
let(:config) { {} }
it { is_expected.to be_nil }
end
end
describe '#find_excluded_keys' do
subject { described_class.new(config: config).find_excluded_keys(klass_name) }
let(:klass_name) { 'project' }
context 'when initialized with excluded_attributes' do
let(:config) { { excluded_attributes: excluded_attributes } }
let(:excluded_attributes) { { project: [:name, :path], issues: [:milestone_id] } }
it { is_expected.to eq(%w[name path]) }
end
context 'when excluded_attributes are not present in config' do
let(:config) { {} }
it { is_expected.to eq([]) }
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
require 'rspec-parameterized'
describe Gitlab::ImportExport::Config do describe Gitlab::ImportExport::Config do
let(:yaml_file) { described_class.new } let(:yaml_file) { described_class.new }
describe '#to_h' do describe '#to_h' do
context 'when using CE' do subject { yaml_file.to_h }
before do
allow(yaml_file) context 'when using default config' do
.to receive(:merge?) using RSpec::Parameterized::TableSyntax
.and_return(false)
where(:ee) do
[true, false]
end end
it 'just returns the parsed Hash without the EE section' do with_them do
expected = YAML.load_file(Gitlab::ImportExport.config_file) before do
expected.delete('ee') allow(Gitlab).to receive(:ee?) { ee }
end
expect(yaml_file.to_h).to eq(expected) it 'parses default config' do
expect { subject }.not_to raise_error
expect(subject).to be_a(Hash)
expect(subject.keys).to contain_exactly(
:tree, :excluded_attributes, :included_attributes, :methods)
end
end end
end end
context 'when using EE' do context 'when using custom config' do
before do let(:config) do
allow(yaml_file) <<-EOF.strip_heredoc
.to receive(:merge?) tree:
.and_return(true) project:
end - labels:
- :priorities
- milestones:
- events:
- :push_event_payload
it 'merges the EE project tree into the CE project tree' do included_attributes:
allow(yaml_file) user:
.to receive(:parse_yaml) - :id
.and_return({
'project_tree' => [
{
'issues' => [
:id,
:title,
{ 'notes' => [:id, :note, { 'author' => [:name] }] }
]
}
],
'ee' => {
'project_tree' => [
{
'issues' => [
:description,
{ 'notes' => [:date, { 'author' => [:email] }] }
]
},
{ 'foo' => [{ 'bar' => %i[baz] }] }
]
}
})
expect(yaml_file.to_h).to eq({ excluded_attributes:
'project_tree' => [ project:
{ - :name
'issues' => [
:id, methods:
:title, labels:
{ - :type
'notes' => [ events:
:id, - :action
:note,
{ 'author' => [:name, :email] }, ee:
:date tree:
] project:
}, protected_branches:
:description - :unprotect_access_levels
] included_attributes:
}, user:
{ 'foo' => [{ 'bar' => %i[baz] }] } - :name_ee
] excluded_attributes:
}) project:
- :name_without_ee
methods:
labels:
- :type_ee
events_ee:
- :action_ee
EOF
end end
it 'merges the excluded attributes list' do let(:config_hash) { YAML.safe_load(config, [Symbol]) }
allow(yaml_file)
.to receive(:parse_yaml) before do
.and_return({ allow_any_instance_of(described_class).to receive(:parse_yaml) do
'project_tree' => [], config_hash.deep_dup
'excluded_attributes' => { end
'project' => %i[id title],
'notes' => %i[id]
},
'ee' => {
'project_tree' => [],
'excluded_attributes' => {
'project' => %i[date],
'foo' => %i[bar baz]
}
}
})
expect(yaml_file.to_h).to eq({
'project_tree' => [],
'excluded_attributes' => {
'project' => %i[id title date],
'notes' => %i[id],
'foo' => %i[bar baz]
}
})
end end
it 'merges the included attributes list' do context 'when using CE' do
allow(yaml_file) before do
.to receive(:parse_yaml) allow(Gitlab).to receive(:ee?) { false }
.and_return({ end
'project_tree' => [],
'included_attributes' => { it 'just returns the normalized Hash' do
'project' => %i[id title], is_expected.to eq(
'notes' => %i[id] {
}, tree: {
'ee' => { project: {
'project_tree' => [], labels: {
'included_attributes' => { priorities: {}
'project' => %i[date], },
'foo' => %i[bar baz] milestones: {
events: {
push_event_payload: {}
}
}
}
},
included_attributes: {
user: [:id]
},
excluded_attributes: {
project: [:name]
},
methods: {
labels: [:type],
events: [:action]
} }
} }
}) )
end
expect(yaml_file.to_h).to eq({
'project_tree' => [],
'included_attributes' => {
'project' => %i[id title date],
'notes' => %i[id],
'foo' => %i[bar baz]
}
})
end end
it 'merges the methods list' do context 'when using EE' do
allow(yaml_file) before do
.to receive(:parse_yaml) allow(Gitlab).to receive(:ee?) { true }
.and_return({ end
'project_tree' => [],
'methods' => { it 'just returns the normalized Hash' do
'project' => %i[id title], is_expected.to eq(
'notes' => %i[id] {
}, tree: {
'ee' => { project: {
'project_tree' => [], labels: {
'methods' => { priorities: {}
'project' => %i[date], },
'foo' => %i[bar baz] milestones: {
events: {
push_event_payload: {}
}
},
protected_branches: {
unprotect_access_levels: {}
}
}
},
included_attributes: {
user: [:id, :name_ee]
},
excluded_attributes: {
project: [:name, :name_without_ee]
},
methods: {
labels: [:type, :type_ee],
events: [:action],
events_ee: [:action_ee]
} }
} }
}) )
end
expect(yaml_file.to_h).to eq({
'project_tree' => [],
'methods' => {
'project' => %i[id title date],
'notes' => %i[id],
'foo' => %i[bar baz]
}
})
end end
end end
end end
......
...@@ -8,7 +8,7 @@ describe 'Import/Export model configuration' do ...@@ -8,7 +8,7 @@ describe 'Import/Export model configuration' do
let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys } let(:config_hash) { Gitlab::ImportExport::Config.new.to_h.deep_stringify_keys }
let(:model_names) do let(:model_names) do
names = names_from_tree(config_hash['project_tree']) names = names_from_tree(config_hash.dig('tree', 'project'))
# Remove duplicated or add missing models # Remove duplicated or add missing models
# - project is not part of the tree, so it has to be added manually. # - project is not part of the tree, so it has to be added manually.
......
...@@ -2,96 +2,45 @@ require 'spec_helper' ...@@ -2,96 +2,45 @@ require 'spec_helper'
describe Gitlab::ImportExport::Reader do describe Gitlab::ImportExport::Reader do
let(:shared) { Gitlab::ImportExport::Shared.new(nil) } let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
let(:test_config) { 'spec/support/import_export/import_export.yml' }
let(:project_tree_hash) do
{
except: [:id, :created_at],
include: [:issues, :labels,
{ merge_requests: {
only: [:id],
except: [:iid],
include: [:merge_request_diff, :merge_request_test]
} },
{ commit_statuses: { include: :commit } },
{ project_members: { include: { user: { only: [:email] } } } }]
}
end
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:config_file).and_return(test_config)
end
it 'generates hash from project tree config' do
expect(described_class.new(shared: shared).project_tree).to match(project_tree_hash)
end
context 'individual scenarios' do
it 'generates the correct hash for a single project relation' do
setup_yaml(project_tree: [:issues])
expect(described_class.new(shared: shared).project_tree).to match(include: [:issues])
end
it 'generates the correct hash for a single project feature relation' do
setup_yaml(project_tree: [:project_feature])
expect(described_class.new(shared: shared).project_tree).to match(include: [:project_feature]) describe '#project_tree' do
end subject { described_class.new(shared: shared).project_tree }
it 'generates the correct hash for a multiple project relation' do it 'delegates to AttributesFinder#find_root' do
setup_yaml(project_tree: [:issues, :snippets]) expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
.to receive(:find_root)
.with(:project)
expect(described_class.new(shared: shared).project_tree).to match(include: [:issues, :snippets]) subject
end end
it 'generates the correct hash for a single sub-relation' do context 'when exception raised' do
setup_yaml(project_tree: [issues: [:notes]]) before do
expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
.to receive(:find_root)
.with(:project)
.and_raise(StandardError)
end
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { include: :notes } }]) it { is_expected.to be false }
end
it 'generates the correct hash for a multiple sub-relation' do
setup_yaml(project_tree: [merge_requests: [:notes, :merge_request_diff]])
expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: [:notes, :merge_request_diff] } }])
end
it 'generates the correct hash for a sub-relation with another sub-relation' do it 'logs the error' do
setup_yaml(project_tree: [merge_requests: [notes: :author]]) expect(shared).to receive(:error).with(instance_of(StandardError))
expect(described_class.new(shared: shared).project_tree).to match(include: [{ merge_requests: { include: { notes: { include: :author } } } }]) subject
end
end end
end
it 'generates the correct hash for a relation with included attributes' do describe '#group_members_tree' do
setup_yaml(project_tree: [:issues], included_attributes: { issues: [:name, :description] }) subject { described_class.new(shared: shared).group_members_tree }
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { only: [:name, :description] } }])
end
it 'generates the correct hash for a relation with excluded attributes' do
setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] })
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name] } }])
end
it 'generates the correct hash for a relation with both excluded and included attributes' do
setup_yaml(project_tree: [:issues], excluded_attributes: { issues: [:name] }, included_attributes: { issues: [:description] })
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { except: [:name], only: [:description] } }])
end
it 'generates the correct hash for a relation with custom methods' do
setup_yaml(project_tree: [:issues], methods: { issues: [:name] })
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
end
it 'generates the correct hash for group members' do it 'delegates to AttributesFinder#find_root' do
expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } }) expect_any_instance_of(Gitlab::ImportExport::AttributesFinder)
end .to receive(:find_root)
.with(:group_members)
def setup_yaml(hash) subject
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
end end
end end
end end
...@@ -12,7 +12,7 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -12,7 +12,7 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:user) { create(:admin) } let(:user) { create(:admin) }
let(:group) { create(:group, :nested) } let(:group) { create(:group, :nested) }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let!(:project) { create(:project, :builds_disabled, :issues_disabled, group: group, name: 'project', path: 'project') }
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
before do before do
...@@ -24,7 +24,6 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -24,7 +24,6 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:import_path) { 'spec/lib/gitlab/import_export' } let(:import_path) { 'spec/lib/gitlab/import_export' }
let(:file_content) { IO.read("#{import_path}/project.json") } let(:file_content) { IO.read("#{import_path}/project.json") }
let!(:json_file) { ActiveSupport::JSON.decode(file_content) } let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
let(:tree_hash) { project_tree_restorer.instance_variable_get(:@tree_hash) }
before do before do
allow(shared).to receive(:export_path).and_return(import_path) allow(shared).to receive(:export_path).and_return(import_path)
...@@ -92,21 +91,25 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -92,21 +91,25 @@ describe Gitlab::ImportExport::RelationRenameService do
end end
context 'when exporting' do context 'when exporting' do
let(:project_tree_saver) { Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: user, shared: shared) } let(:export_content_path) { project_tree_saver.full_path }
let(:project_tree) { project_tree_saver.send(:project_json) } let(:export_content_hash) { ActiveSupport::JSON.decode(File.read(export_content_path)) }
let(:injected_hash) { renames.values.product([{}]).to_h }
it 'adds old relationships to the exported file' do let(:project_tree_saver) do
project_tree.merge!(renames.values.map { |new_name| [new_name, []] }.to_h) Gitlab::ImportExport::ProjectTreeSaver.new(
project: project, current_user: user, shared: shared)
end
allow(project_tree_saver).to receive(:save) do |arg| it 'adds old relationships to the exported file' do
project_tree_saver.send(:project_json_tree) # we inject relations with new names that should be rewritten
expect(project_tree_saver).to receive(:serialize_project_tree).and_wrap_original do |method, *args|
method.call(*args).merge(injected_hash)
end end
result = project_tree_saver.save expect(project_tree_saver.save).to eq(true)
saved_data = ActiveSupport::JSON.decode(result)
expect(saved_data.keys).to include(*(renames.keys + renames.values)) expect(export_content_hash.keys).to include(*renames.keys)
expect(export_content_hash.keys).to include(*renames.values)
end end
end end
end end
# Class relationships to be included in the project import/export # Class relationships to be included in the project import/export
project_tree: tree:
- :issues project:
- :labels - :issues
- merge_requests: - :labels
- :merge_request_diff - merge_requests:
- :merge_request_test - :merge_request_diff
- commit_statuses: - :merge_request_test
- :commit - commit_statuses:
- project_members: - :commit
- project_members:
- :user
group_members:
- :user - :user
included_attributes: included_attributes:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment