diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 06252c7b625..535da706159 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -26,7 +26,7 @@ module Projects end def project_tree_saver - Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared) + Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared) end def uploads_saver diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml new file mode 100644 index 00000000000..fe580af31b3 --- /dev/null +++ b/changelogs/unreleased/fix-import-group-members.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to export project inherited group members to Import/Export +merge_request: 8923 +author: diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index cb1c1a84f8c..be042ddf623 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -14,6 +14,11 @@ > raketask. > - The exports are stored in a temporary [shared directory][tmp] and are deleted > every 24 hours by a specific worker. +> - Group members will get exported as project members, as long as the user has +> master or admin access to the group where the exported project lives. An admin +> in the import side is required to map the users, based on email or username. +> Otherwise, a supplementary comment is left to mention the original author and +> the MRs, notes or issues will be owned by the importer. Existing projects running on any GitLab instance or GitLab.com can be exported with all their related data and be moved into a new GitLab instance. @@ -22,7 +27,7 @@ with all their related data and be moved into a new GitLab instance. | GitLab version | Import/Export version | | -------- | -------- | -| 8.16.2 to current | 0.1.6 | +| 8.17.0 to current | 0.1.6 | | 8.13.0 | 0.1.5 | | 8.12.0 | 0.1.4 | | 8.10.3 | 0.1.3 | diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index a09577ae48d..8b8e48aac76 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -32,6 +32,10 @@ module Gitlab @user.id end + def include?(old_author_id) + map.keys.include?(old_author_id) && map[old_author_id] != default_user_id + end + private def missing_keys_tracking_hash diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index 2fbf437ec26..b79be62245b 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -5,8 +5,9 @@ module Gitlab attr_reader :full_path - def initialize(project:, shared:) + def initialize(project:, current_user:, shared:) @project = project + @current_user = current_user @shared = shared @full_path = File.join(@shared.export_path, ImportExport.project_filename) end @@ -24,7 +25,29 @@ module Gitlab private def project_json_tree - @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree) + project_json['project_members'] += group_members_json + + project_json.to_json + end + + def project_json + @project_json ||= @project.as_json(reader.project_tree) + end + + def reader + @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared) + end + + def group_members_json + 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 + end + end + + def group_members + return [] unless @current_user.can?(:admin_group, @project.group) + + MembersFinder.new(@project.project_members, @project.group).execute(@current_user) end end end diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb index 5021a1a14ce..a1e7159fe42 100644 --- a/lib/gitlab/import_export/reader.rb +++ b/lib/gitlab/import_export/reader.rb @@ -21,6 +21,10 @@ module Gitlab false end + def group_members_tree + @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user)) + end + private # Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 0319d7707a8..fae792237d9 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -89,7 +89,7 @@ module Gitlab end def has_author?(old_author_id) - admin_user? && @members_mapper.map.keys.include?(old_author_id) + admin_user? && @members_mapper.include?(old_author_id) end def missing_author_note(updated_at, author_name) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index f2cb028206f..b9d4e59e770 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -116,5 +116,27 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end end + + context 'importing group members' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, namespace: group) } + let(:members_mapper) do + described_class.new( + exported_members: exported_members, user: user, project: project) + end + + before do + group.add_users([user, user2], GroupMember::DEVELOPER) + user.update(email: 'invite@test.com') + end + + it 'maps the importer' do + expect(members_mapper.map[-1]).to eq(user.id) + end + + it 'maps the group member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 550daa44010..3628adefc0c 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeSaver, services: true do describe 'saves the project tree into a json object' do let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) } - let(:project_tree_saver) { described_class.new(project: project, shared: shared) } + let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } let(:user) { create(:user) } let(:project) { setup_project } @@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end it 'has pipeline builds' do - expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1) + expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1) end it 'has pipeline commits' do @@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do end it 'has project and group labels' do - label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']} + label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] } expect(label_types).to match_array(['ProjectLabel', 'GroupLabel']) end it 'has priorities associated to labels' do - priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']} + priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities'] } expect(priorities.flatten).not_to be_empty end @@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(project_tree_saver.save).to be true end + + context 'group members' do + let(:user2) { create(:user, email: 'group@member.com') } + let(:member_emails) do + saved_project_json['project_members'].map do |pm| + pm['user']['email'] + end + end + + before do + Group.first.add_developer(user2) + end + + it 'does not export group members if it has no permission' do + Group.first.add_developer(user) + + expect(member_emails).not_to include('group@member.com') + end + + it 'does not export group members as master' do + Group.first.add_master(user) + + expect(member_emails).not_to include('group@member.com') + end + + it 'exports group members as group owner' do + Group.first.add_owner(user) + + expect(member_emails).to include('group@member.com') + end + + context 'as admin' do + let(:user) { create(:admin) } + + it 'exports group members as admin' do + expect(member_emails).to include('group@member.com') + end + + it 'exports group members as project members' do + member_types = saved_project_json['project_members'].map { |pm| pm['source_type'] } + + expect(member_types).to all(eq('Project')) + end + end + end end end @@ -170,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do commit_status = create(:commit_status, project: project) ci_pipeline = create(:ci_pipeline, - project: project, - sha: merge_request.diff_head_sha, - ref: merge_request.source_branch, - statuses: [commit_status]) + project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + statuses: [commit_status]) create(:ci_build, pipeline: ci_pipeline, project: project) create(:milestone, project: project) diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 3ceb1e7e803..48d74b07e27 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -86,6 +86,10 @@ describe Gitlab::ImportExport::Reader, lib: true do expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }]) end + it 'generates the correct hash for group members' do + expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } }) + end + def setup_yaml(hash) allow(YAML).to receive(:load_file).with(test_config).and_return(hash) end