Commit 052ef2a4 authored by Imre Farkas's avatar Imre Farkas Committed by James Lopez

Add Epics to Project import/export

As Epics are Group level objects,
ImportExport::GroupProjectObjectBuilder tries to find existing one on
the Group level, and falls back to creating one if not found.
parent e74f59af
---
title: Add epics to project import/export
merge_request: 19883
author:
type: added
...@@ -5,19 +5,80 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -5,19 +5,80 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
include ImportExport::CommonUtil include ImportExport::CommonUtil
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let_it_be(:user) { create(:admin, username: 'user_1') }
let_it_be(:second_user) { create(:user, username: 'user_2' )}
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
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 }
before do subject(:restored_project_json) { project_tree_restorer.restore }
setup_import_export_config('designs', 'ee')
describe 'epics' do
let_it_be(:user) { create(:user)}
before do
setup_import_export_config('group')
end
context 'with group' do
let!(:project) do
create(:project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE))
end
context 'with pre-existing epic' do
let!(:epic) { create(:epic, title: 'An epic', group: project.group) }
it 'associates epics' do
project = Project.find_by_path('project')
expect { restored_project_json }.not_to change { Epic.count }
expect(project.group.epics.count).to eq(1)
expect(project.issues.find_by_title('Issue with Epic').epic).not_to be_nil
end
end
context 'without pre-existing epic' do
it 'creates epic' do
project = Project.find_by_path('project')
expect { restored_project_json }.to change { Epic.count }.from(0).to(1)
expect(project.group.epics.count).to eq(1)
expect(project.issues.find_by_title('Issue with Epic').epic).not_to be_nil
end
end
end
context 'with personal namespace' do
let!(:project) do
create(:project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
namespace: user.namespace)
end
it 'ignores epic relation' do
project = Project.find_by_path('project')
expect { restored_project_json }.not_to change { Epic.count }
expect(project.import_failures.size).to eq(0)
end
end
end end
describe 'restoring design management data' do describe 'restoring design management data' do
let_it_be(:user) { create(:admin, username: 'user_1') }
let_it_be(:second_user) { create(:user, username: 'user_2' )}
let_it_be(:project) do
create(:project, :builds_disabled, :issues_disabled,
{ name: 'project', path: 'project' })
end
before do before do
setup_import_export_config('designs', 'ee')
restored_project_json restored_project_json
end end
......
...@@ -4,12 +4,18 @@ require 'spec_helper' ...@@ -4,12 +4,18 @@ require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver do describe Gitlab::ImportExport::ProjectTreeSaver do
describe 'saves the project tree into a json object' do describe 'saves the project tree into a json object' do
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:project) { create(:project) } let_it_be(:group) { create(:group) }
set(:issue) { create(:issue, project: project) } let_it_be(:project) { create(:project, group: group) }
set(:design) { create(:design, :with_file, versions_count: 2, issue: issue) } let_it_be(:issue) { create(:issue, project: project) }
set(:note) { create(:diff_note_on_design, noteable: design, project: project, author: user) }
set(:note2) { create(:note, noteable: issue, project: project, author: user) } let_it_be(:design) { create(:design, :with_file, versions_count: 2, issue: issue) }
let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project, author: user) }
let_it_be(:note2) { create(:note, noteable: issue, project: project, author: user) }
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:epic_issue) { create(:epic_issue, issue: issue, epic: epic) }
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec_ee" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec_ee" }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
...@@ -52,6 +58,16 @@ describe Gitlab::ImportExport::ProjectTreeSaver do ...@@ -52,6 +58,16 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end end
end end
end end
context 'epics' do
it 'has issue epic' do
expect(saved_project_json['issues'].first['epic']).not_to be_empty
end
it 'has issue epic id' do
expect(saved_project_json['issues'].first['epic']['id']).to eql(epic.id)
end
end
end end
def project_json(filename) def project_json(filename)
......
...@@ -26,6 +26,8 @@ module Gitlab ...@@ -26,6 +26,8 @@ module Gitlab
end end
def find def find
return if epic? && group.nil?
find_object || klass.create(project_attributes) find_object || klass.create(project_attributes)
end end
...@@ -54,10 +56,10 @@ module Gitlab ...@@ -54,10 +56,10 @@ module Gitlab
# or, if group is present: # or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}` # `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base def where_clause_base
clause = table[:project_id].eq(project.id) if project [].tap do |clauses|
clause = clause.or(table[:group_id].eq(group.id)) if group clauses << table[:project_id].eq(project.id) if project
clauses << table[:group_id].eq(group.id) if group
clause end.reduce(:or)
end end
# Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'` # Returns Arel clause `"{table_name}"."title" = '{attributes['title']}'`
...@@ -108,6 +110,10 @@ module Gitlab ...@@ -108,6 +110,10 @@ module Gitlab
klass == MergeRequest klass == MergeRequest
end end
def epic?
klass == Epic
end
# If an existing group milestone used the IID # If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available # claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following: # This is necessary to fix situations like the following:
......
...@@ -322,6 +322,13 @@ excluded_attributes: ...@@ -322,6 +322,13 @@ excluded_attributes:
- :board_id - :board_id
- :label_id - :label_id
- :milestone_id - :milestone_id
epic:
- :start_date_sourcing_milestone_id
- :due_date_sourcing_milestone_id
- :parent_id
- :state_id
- :start_date_sourcing_epic_id
- :due_date_sourcing_epic_id
methods: methods:
notes: notes:
- :type - :type
...@@ -374,6 +381,7 @@ ee: ...@@ -374,6 +381,7 @@ ee:
- design_versions: - design_versions:
- actions: - actions:
- :design # Duplicate export of issues.designs in order to link the record to both Issue and Action - :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
- :epic
- protected_branches: - protected_branches:
- :unprotect_access_levels - :unprotect_access_levels
- protected_environments: - protected_environments:
......
...@@ -40,7 +40,21 @@ module Gitlab ...@@ -40,7 +40,21 @@ module Gitlab
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 merge_request ProjectCiCdSetting container_expiration_policy].freeze EXISTING_OBJECT_CHECK = %i[
milestone
milestones
label
labels
project_label
project_labels
group_label
group_labels
project_feature
merge_request
epic
ProjectCiCdSetting
container_expiration_policy
].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
......
...@@ -4,7 +4,7 @@ module Gitlab ...@@ -4,7 +4,7 @@ module Gitlab
module ImportExport module ImportExport
class RelationTreeRestorer class RelationTreeRestorer
# Relations which cannot be saved at project level (and have a group assigned) # Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone].freeze GROUP_MODELS = [GroupLabel, Milestone, Epic].freeze
attr_reader :user attr_reader :user
attr_reader :shared attr_reader :shared
......
...@@ -175,6 +175,67 @@ ...@@ -175,6 +175,67 @@
} }
} }
] ]
},
{
"id": 3,
"title": "Issue with Epic",
"author_id": 1,
"project_id": 8,
"created_at": "2019-12-08T19:41:11.233Z",
"updated_at": "2019-12-08T19:41:53.194Z",
"position": 0,
"branch_name": null,
"description": "Donec at nulla vitae sem molestie rutrum ut at sem.",
"state": "opened",
"iid": 3,
"updated_by_id": null,
"confidential": false,
"due_date": null,
"moved_to_id": null,
"issue_assignees": [],
"notes": [],
"milestone": {
"id": 2,
"title": "A group milestone",
"description": "Group-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"group_id": 100
},
"epic": {
"id": 1,
"group_id": 5,
"author_id": 1,
"assignee_id": null,
"iid": 1,
"updated_by_id": null,
"last_edited_by_id": null,
"lock_version": 0,
"start_date": null,
"end_date": null,
"last_edited_at": null,
"created_at": "2019-12-08T19:37:07.098Z",
"updated_at": "2019-12-08T19:43:11.568Z",
"title": "An epic",
"description": null,
"start_date_sourcing_milestone_id": null,
"due_date_sourcing_milestone_id": null,
"start_date_fixed": null,
"due_date_fixed": null,
"start_date_is_fixed": null,
"due_date_is_fixed": null,
"closed_by_id": null,
"closed_at": null,
"parent_id": null,
"relative_position": null,
"state_id": "opened",
"start_date_sourcing_epic_id": null,
"due_date_sourcing_epic_id": null,
"milestone_id": null
}
} }
], ],
"snippets": [ "snippets": [
......
...@@ -578,3 +578,30 @@ zoom_meetings: ...@@ -578,3 +578,30 @@ zoom_meetings:
sentry_issue: sentry_issue:
- issue - issue
design_versions: *version design_versions: *version
epic:
- subscriptions
- award_emoji
- description_versions
- author
- assignee
- issues
- epic_issues
- milestone
- notes
- label_links
- labels
- todos
- metrics
- group
- parent
- children
- updated_by
- last_edited_by
- closed_by
- start_date_sourcing_milestone
- due_date_sourcing_milestone
- start_date_sourcing_epic
- due_date_sourcing_epic
- events
- resource_label_events
- user_mentions
\ No newline at end of file
...@@ -502,7 +502,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -502,7 +502,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
it_behaves_like 'restores project successfully', it_behaves_like 'restores project successfully',
issues: 2, issues: 3,
labels: 2, labels: 2,
label_with_priorities: 'A project label', label_with_priorities: 'A project label',
milestones: 2, milestones: 2,
...@@ -515,7 +515,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -515,7 +515,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'restores issue states' do it 'restores issue states' do
expect(project.issues.with_state(:closed).count).to eq(1) expect(project.issues.with_state(:closed).count).to eq(1)
expect(project.issues.with_state(:opened).count).to eq(1) expect(project.issues.with_state(:opened).count).to eq(2)
end end
end end
......
...@@ -766,3 +766,33 @@ ContainerExpirationPolicy: ...@@ -766,3 +766,33 @@ ContainerExpirationPolicy:
- older_than - older_than
- keep_n - keep_n
- enabled - enabled
Epic:
- id
- milestone_id
- group_id
- author_id
- assignee_id
- iid
- updated_by_id
- last_edited_by_id
- lock_version
- start_date
- end_date
- last_edited_at
- created_at
- updated_at
- title
- description
- start_date_sourcing_milestone_id
- due_date_sourcing_milestone_id
- start_date_fixed
- due_date_fixed
- start_date_is_fixed
- due_date_is_fixed
- closed_by_id
- closed_at
- parent_id
- relative_position
- state_id
- start_date_sourcing_epic_id
- due_date_sourcing_epic_id
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