Merge branch 'fix/import-group-members' into 'master'
Fix missing group members from Import/Export Closes #25124 See merge request !8923
This commit is contained in:
commit
95a3e2da9b
|
@ -26,7 +26,7 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_tree_saver
|
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
|
end
|
||||||
|
|
||||||
def uploads_saver
|
def uploads_saver
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add ability to export project inherited group members to Import/Export
|
||||||
|
merge_request: 8923
|
||||||
|
author:
|
|
@ -14,6 +14,11 @@
|
||||||
> raketask.
|
> raketask.
|
||||||
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
|
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
|
||||||
> every 24 hours by a specific worker.
|
> 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
|
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.
|
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 |
|
| 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.13.0 | 0.1.5 |
|
||||||
| 8.12.0 | 0.1.4 |
|
| 8.12.0 | 0.1.4 |
|
||||||
| 8.10.3 | 0.1.3 |
|
| 8.10.3 | 0.1.3 |
|
||||||
|
|
|
@ -32,6 +32,10 @@ module Gitlab
|
||||||
@user.id
|
@user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include?(old_author_id)
|
||||||
|
map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def missing_keys_tracking_hash
|
def missing_keys_tracking_hash
|
||||||
|
|
|
@ -5,8 +5,9 @@ module Gitlab
|
||||||
|
|
||||||
attr_reader :full_path
|
attr_reader :full_path
|
||||||
|
|
||||||
def initialize(project:, shared:)
|
def initialize(project:, current_user:, shared:)
|
||||||
@project = project
|
@project = project
|
||||||
|
@current_user = current_user
|
||||||
@shared = shared
|
@shared = shared
|
||||||
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
|
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
|
||||||
end
|
end
|
||||||
|
@ -24,7 +25,29 @@ module Gitlab
|
||||||
private
|
private
|
||||||
|
|
||||||
def project_json_tree
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,10 @@ module Gitlab
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_members_tree
|
||||||
|
@attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
||||||
|
|
|
@ -89,7 +89,7 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_author?(old_author_id)
|
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
|
end
|
||||||
|
|
||||||
def missing_author_note(updated_at, author_name)
|
def missing_author_note(updated_at, author_name)
|
||||||
|
|
|
@ -116,5 +116,27 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
|
||||||
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
|
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
||||||
describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
||||||
describe 'saves the project tree into a json object' do
|
describe 'saves the project tree into a json object' do
|
||||||
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
|
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(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:project) { setup_project }
|
let(:project) { setup_project }
|
||||||
|
@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has pipeline builds' do
|
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
|
end
|
||||||
|
|
||||||
it 'has pipeline commits' do
|
it 'has pipeline commits' do
|
||||||
|
@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has project and group labels' do
|
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'])
|
expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has priorities associated to labels' do
|
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
|
expect(priorities.flatten).not_to be_empty
|
||||||
end
|
end
|
||||||
|
@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
||||||
|
|
||||||
expect(project_tree_saver.save).to be true
|
expect(project_tree_saver.save).to be true
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -170,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
|
||||||
commit_status = create(:commit_status, project: project)
|
commit_status = create(:commit_status, project: project)
|
||||||
|
|
||||||
ci_pipeline = create(:ci_pipeline,
|
ci_pipeline = create(:ci_pipeline,
|
||||||
project: project,
|
project: project,
|
||||||
sha: merge_request.diff_head_sha,
|
sha: merge_request.diff_head_sha,
|
||||||
ref: merge_request.source_branch,
|
ref: merge_request.source_branch,
|
||||||
statuses: [commit_status])
|
statuses: [commit_status])
|
||||||
|
|
||||||
create(:ci_build, pipeline: ci_pipeline, project: project)
|
create(:ci_build, pipeline: ci_pipeline, project: project)
|
||||||
create(:milestone, project: project)
|
create(:milestone, project: project)
|
||||||
|
|
|
@ -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] } }])
|
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
|
||||||
end
|
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)
|
def setup_yaml(hash)
|
||||||
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
|
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue