Commit b6689e03 authored by Luke Duncalfe's avatar Luke Duncalfe

CE-specific changes to allow exporting Design data

Moving a group of shared_examples from project_tree_restorer_spec to
its own file in spec/support/shared_examples in order for these examples
to be reused in an EE-specific test.

https://gitlab.com/gitlab-org/gitlab-ee/issues/11090
parent 35ab27c2
# frozen_string_literal: true
module ExportHelper
# An EE-overwriteable list of descriptions
def project_export_descriptions
[
_('Project and wiki repositories'),
_('Project uploads'),
_('Project configuration, including services'),
_('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities'),
_('LFS objects'),
_('Issue Boards')
]
end
end
ExportHelper.prepend_if_ee('EE::ExportHelper')
...@@ -10,12 +10,8 @@ ...@@ -10,12 +10,8 @@
%p.append-bottom-0 %p.append-bottom-0
%p= _('The following items will be exported:') %p= _('The following items will be exported:')
%ul %ul
%li= _('Project and wiki repositories') - project_export_descriptions.each do |desc|
%li= _('Project uploads') %li= desc
%li= _('Project configuration, including services')
%li= _('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities')
%li= _('LFS objects')
%li= _('Issue Boards')
%p= _('The following items will NOT be exported:') %p= _('The following items will NOT be exported:')
%ul %ul
%li= _('Job traces and artifacts') %li= _('Job traces and artifacts')
......
# frozen_string_literal: true
class DesignIssueIdNullable < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
change_column_null :design_management_designs, :issue_id, true
end
end
...@@ -1217,7 +1217,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do ...@@ -1217,7 +1217,7 @@ ActiveRecord::Schema.define(version: 2019_09_26_041216) do
create_table "design_management_designs", force: :cascade do |t| create_table "design_management_designs", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.integer "issue_id", null: false t.integer "issue_id"
t.string "filename", null: false t.string "filename", null: false
t.index ["issue_id", "filename"], name: "index_design_management_designs_on_issue_id_and_filename", unique: true t.index ["issue_id", "filename"], name: "index_design_management_designs_on_issue_id_and_filename", unique: true
t.index ["project_id"], name: "index_design_management_designs_on_project_id" t.index ["project_id"], name: "index_design_management_designs_on_project_id"
......
...@@ -38,7 +38,6 @@ to be enabled: ...@@ -38,7 +38,6 @@ to be enabled:
- Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`. - Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`.
The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab/issues/12771). The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab/issues/12771).
- Design uploads are limited to 10 files at a time. - Design uploads are limited to 10 files at a time.
- [Designs cannot yet be deleted](https://gitlab.com/gitlab-org/gitlab/issues/11089).
- Design Management is - Design Management is
[not yet supported in the project export](https://gitlab.com/gitlab-org/gitlab/issues/11090). [not yet supported in the project export](https://gitlab.com/gitlab-org/gitlab/issues/11090).
- Design Management data - Design Management data
......
...@@ -65,6 +65,7 @@ The following items will be exported: ...@@ -65,6 +65,7 @@ The following items will be exported:
- Project configuration, including services - Project configuration, including services
- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, - Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
and other project entities and other project entities
- Design Management files and data **(PREMIUM)**
- LFS objects - LFS objects
- Issue boards - Issue boards
......
...@@ -26,30 +26,60 @@ module Gitlab ...@@ -26,30 +26,60 @@ module Gitlab
end end
def find def find
find_object || @klass.create(project_attributes) find_object || klass.create(project_attributes)
end end
private private
attr_reader :klass, :attributes, :group, :project
def find_object def find_object
@klass.where(where_clause).first klass.where(where_clause).first
end end
def where_clause def where_clause
@attributes.slice('title').map do |key, value| where_clauses.reduce(:and)
scope_clause = table[:project_id].eq(@project.id) end
scope_clause = scope_clause.or(table[:group_id].eq(@group.id)) if @group
def where_clauses
[
where_clause_base,
where_clause_for_title,
where_clause_for_klass
].compact
end
# Returns Arel clause `"{table_name}"."project_id" = {project.id}`
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
clause = table[:project_id].eq(project.id)
clause = clause.or(table[:group_id].eq(group.id)) if group
clause
end
table[key].eq(value).and(scope_clause) # Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'`
end.reduce(:or) # if attributes has 'title key, otherwise `nil`.
def where_clause_for_title
attrs_to_arel(attributes.slice('title'))
end
# Returns Arel clause:
# `"{table_name}"."{attrs.keys[0]}" = '{attrs.values[0]} AND {table_name}"."{attrs.keys[1]}" = '{attrs.values[1]}"`
# from the given Hash of attributes.
def attrs_to_arel(attrs)
attrs.map do |key, value|
table[key].eq(value)
end.reduce(:and)
end end
def table def table
@table ||= @klass.arel_table @table ||= klass.arel_table
end end
def project_attributes def project_attributes
@attributes.except('group').tap do |atts| attributes.except('group').tap do |atts|
if label? if label?
atts['type'] = 'ProjectLabel' # Always create project labels atts['type'] = 'ProjectLabel' # Always create project labels
elsif milestone? elsif milestone?
...@@ -60,15 +90,17 @@ module Gitlab ...@@ -60,15 +90,17 @@ module Gitlab
claim_iid claim_iid
end end
end end
atts['importing'] = true if klass.ancestors.include?(Importable)
end end
end end
def label? def label?
@klass == Label klass == Label
end end
def milestone? def milestone?
@klass == Milestone klass == Milestone
end end
# If an existing group milestone used the IID # If an existing group milestone used the IID
...@@ -79,7 +111,7 @@ module Gitlab ...@@ -79,7 +111,7 @@ module Gitlab
def claim_iid def claim_iid
# The milestone has to be a group milestone, as it's the only case where # The milestone has to be a group milestone, as it's the only case where
# we set the IID as the maximum. The rest of them are fixed. # we set the IID as the maximum. The rest of them are fixed.
milestone = @project.milestones.find_by(iid: @attributes['iid']) milestone = project.milestones.find_by(iid: attributes['iid'])
return unless milestone return unless milestone
...@@ -87,6 +119,13 @@ module Gitlab ...@@ -87,6 +119,13 @@ module Gitlab
milestone.ensure_project_iid! milestone.ensure_project_iid!
milestone.save! milestone.save!
end end
protected
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
# no-op
end
end end
end end
end end
...@@ -248,7 +248,16 @@ preloads: ...@@ -248,7 +248,16 @@ preloads:
ee: ee:
tree: tree:
project: project:
protected_branches: - issues:
- designs:
- notes:
- :author
- events:
- :push_event_payload
- design_versions:
- actions:
- :design # Duplicate export of issues.designs in order to link the record to both Issue and DesignVersion
- protected_branches:
- :unprotect_access_levels - :unprotect_access_levels
protected_environments: - protected_environments:
- :deploy_access_levels - :deploy_access_levels
...@@ -93,6 +93,10 @@ module Gitlab ...@@ -93,6 +93,10 @@ module Gitlab
end end
end end
def remove_feature_dependent_sub_relations(_relation_item)
# no-op
end
def project_relations_without_project_members def project_relations_without_project_members
# We remove `project_members` as they are deserialized separately # We remove `project_members` as they are deserialized separately
project_relations.except(:project_members) project_relations.except(:project_members)
...@@ -171,6 +175,8 @@ module Gitlab ...@@ -171,6 +175,8 @@ module Gitlab
next next
end end
remove_feature_dependent_sub_relations(relation_item)
# The transaction at this level is less speedy than one single transaction # The transaction at this level is less speedy than one single transaction
# 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.
...@@ -238,3 +244,5 @@ module Gitlab ...@@ -238,3 +244,5 @@ module Gitlab
end end
end end
end end
Gitlab::ImportExport::ProjectTreeRestorer.prepend_if_ee('::EE::Gitlab::ImportExport::ProjectTreeRestorer')
...@@ -34,13 +34,13 @@ module Gitlab ...@@ -34,13 +34,13 @@ module Gitlab
PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze BUILD_MODELS = %i[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature].freeze
TOKEN_RESET_MODELS = %w[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
def self.create(*args) def self.create(*args)
new(*args).create new(*args).create
...@@ -56,7 +56,7 @@ module Gitlab ...@@ -56,7 +56,7 @@ module Gitlab
end end
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: []) def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: [])
@relation_name = self.class.overrides[relation_sym] || relation_sym @relation_name = self.class.overrides[relation_sym]&.to_sym || relation_sym
@relation_hash = relation_hash.except('noteable_id') @relation_hash = relation_hash.except('noteable_id')
@members_mapper = members_mapper @members_mapper = members_mapper
@user = user @user = user
...@@ -92,6 +92,10 @@ module Gitlab ...@@ -92,6 +92,10 @@ module Gitlab
OVERRIDES OVERRIDES
end end
def self.existing_object_check
EXISTING_OBJECT_CHECK
end
private private
def setup_models def setup_models
...@@ -105,7 +109,7 @@ module Gitlab ...@@ -105,7 +109,7 @@ module Gitlab
update_group_references update_group_references
remove_duplicate_assignees remove_duplicate_assignees
setup_pipeline if @relation_name == 'Ci::Pipeline' setup_pipeline if @relation_name == :'Ci::Pipeline'
reset_tokens! reset_tokens!
remove_encrypted_attributes! remove_encrypted_attributes!
...@@ -184,14 +188,14 @@ module Gitlab ...@@ -184,14 +188,14 @@ module Gitlab
end end
def update_group_references def update_group_references
return unless EXISTING_OBJECT_CHECK.include?(@relation_name) return unless self.class.existing_object_check.include?(@relation_name)
return unless @relation_hash['group_id'] return unless @relation_hash['group_id']
@relation_hash['group_id'] = @project.namespace_id @relation_hash['group_id'] = @project.namespace_id
end end
def reset_tokens! def reset_tokens!
return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s) return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name)
# If we import/export a project to the same instance, tokens will have to be reset. # If we import/export a project 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. # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
...@@ -255,7 +259,7 @@ module Gitlab ...@@ -255,7 +259,7 @@ module Gitlab
# Only find existing records to avoid mapping tables such as milestones # Only find existing records to avoid mapping tables such as milestones
# Otherwise always create the record, skipping the extra SELECT clause. # Otherwise always create the record, skipping the extra SELECT clause.
@existing_or_new_object ||= begin @existing_or_new_object ||= begin
if EXISTING_OBJECT_CHECK.include?(@relation_name) if self.class.existing_object_check.include?(@relation_name)
attribute_hash = attribute_hash_for(['events']) attribute_hash = attribute_hash_for(['events'])
existing_object.assign_attributes(attribute_hash) if attribute_hash.any? existing_object.assign_attributes(attribute_hash) if attribute_hash.any?
...@@ -284,7 +288,7 @@ module Gitlab ...@@ -284,7 +288,7 @@ module Gitlab
end end
def legacy_trigger? def legacy_trigger?
@relation_name == 'Ci::Trigger' && @relation_hash['owner_id'].nil? @relation_name == :'Ci::Trigger' && @relation_hash['owner_id'].nil?
end end
def find_or_create_object! def find_or_create_object!
...@@ -293,7 +297,7 @@ module Gitlab ...@@ -293,7 +297,7 @@ module Gitlab
# Can't use IDs as validation exists calling `group` or `project` attributes # Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash| finder_hash = parsed_relation_hash.tap do |hash|
hash['group'] = @project.group if relation_class.attribute_method?('group_id') hash['group'] = @project.group if relation_class.attribute_method?('group_id')
hash['project'] = @project hash['project'] = @project if relation_class.reflect_on_association(:project)
hash.delete('project_id') hash.delete('project_id')
end end
......
...@@ -5179,6 +5179,9 @@ msgstr "" ...@@ -5179,6 +5179,9 @@ msgstr ""
msgid "Deselect all" msgid "Deselect all"
msgstr "" msgstr ""
msgid "Design Management files and data"
msgstr ""
msgid "DesignManagement|%{current_design} of %{designs_count}" msgid "DesignManagement|%{current_design} of %{designs_count}"
msgstr "" msgstr ""
......
...@@ -502,3 +502,17 @@ lists: ...@@ -502,3 +502,17 @@ lists:
milestone_releases: milestone_releases:
- milestone - milestone
- release - release
design: &design
- issue
- actions
- versions
- notes
designs: *design
actions:
- design
- version
versions: &version
- issue
- designs
- actions
design_versions: *version
...@@ -2,6 +2,8 @@ require 'spec_helper' ...@@ -2,6 +2,8 @@ require 'spec_helper'
include ImportExport::CommonUtil include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:shared) { project.import_export_shared }
describe 'restore project tree' do describe 'restore project tree' do
before(:context) do before(:context) do
# Using an admin for import, so we can check assignment of existing members # Using an admin for import, so we can check assignment of existing members
...@@ -274,36 +276,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -274,36 +276,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
end end
shared_examples 'restores project successfully' do
it 'correctly restores project' do
expect(shared.errors).to be_empty
expect(restored_project_json).to be_truthy
end
end
shared_examples 'restores project correctly' do |**results|
it 'has labels' do
expect(project.labels.size).to eq(results.fetch(:labels, 0))
end
it 'has label priorities' do
expect(project.labels.find_by(title: 'A project label').priorities).not_to be_empty
end
it 'has milestones' do
expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
end
it 'has issues' do
expect(project.issues.size).to eq(results.fetch(:issues, 0))
end
it 'does not set params that are excluded from import_export settings' do
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
end
end
shared_examples 'restores group correctly' do |**results| shared_examples 'restores group correctly' do |**results|
it 'has group label' do it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
...@@ -322,7 +294,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -322,7 +294,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'Light JSON' do context 'Light JSON' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:shared) { project.import_export_shared }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
...@@ -341,6 +312,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -341,6 +312,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly', it_behaves_like 'restores project correctly',
issues: 1, issues: 1,
labels: 2, labels: 2,
label_with_priorities: 'A project label',
milestones: 1, milestones: 1,
first_issue_labels: 1, first_issue_labels: 1,
services: 1 services: 1
...@@ -363,7 +335,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -363,7 +335,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
create(:ci_build, token: 'abcd') create(:ci_build, token: 'abcd')
end end
it_behaves_like 'restores project successfully' it_behaves_like 'restores project correctly',
issues: 1,
labels: 2,
label_with_priorities: 'A project label',
milestones: 1,
first_issue_labels: 1
end end
end end
...@@ -435,10 +412,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -435,10 +412,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
restored_project_json restored_project_json
end end
it_behaves_like 'restores project successfully'
it_behaves_like 'restores project correctly', it_behaves_like 'restores project correctly',
issues: 2, issues: 2,
labels: 2, labels: 2,
label_with_priorities: 'A project label',
milestones: 2, milestones: 2,
first_issue_labels: 1 first_issue_labels: 1
...@@ -534,7 +511,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -534,7 +511,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
describe '#restored_project' do describe '#restored_project' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let(:tree_hash) { { 'visibility_level' => visibility } } let(:tree_hash) { { 'visibility_level' => visibility } }
let(:restorer) { described_class.new(user: nil, shared: shared, project: project) } let(:restorer) { described_class.new(user: nil, shared: shared, project: project) }
......
...@@ -731,3 +731,18 @@ ExternalPullRequest: ...@@ -731,3 +731,18 @@ ExternalPullRequest:
- target_repository - target_repository
- source_sha - source_sha
- target_sha - target_sha
DesignManagement::Design:
- id
- project_id
- issue_id
- filename
DesignManagement::Action:
- design_id
- event
- version_id
DesignManagement::Version:
- id
- created_at
- sha
- issue_id
- user_id
# frozen_string_literal: true
# Shared examples for ProjectTreeRestorer (shared to allow the testing
# of EE-specific features)
RSpec.shared_examples 'restores project correctly' do |**results|
it 'restores the project' do
expect(shared.errors).to be_empty
expect(restored_project_json).to be_truthy
end
it 'has labels' do
labels_size = results.fetch(:labels, 0)
expect(project.labels.size).to eq(labels_size)
end
it 'has label priorities' do
label_with_priorities = results[:label_with_priorities]
if label_with_priorities
expect(project.labels.find_by(title: label_with_priorities).priorities).not_to be_empty
end
end
it 'has milestones' do
expect(project.milestones.size).to eq(results.fetch(:milestones, 0))
end
it 'has issues' do
expect(project.issues.size).to eq(results.fetch(:issues, 0))
end
it 'does not set params that are excluded from import_export settings' do
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
end
end
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