Commit 3948d1ff authored by George Koltsov's avatar George Koltsov

Migrate group milestones when using Bulk Import

- Add group milestones relation to Group Migration tool
  in order to provide closer feature parity to Group
  Import/Export
parent 9c4e1472
---
title: Migrate group milestones when using Bulk Import
merge_request: 55981
author:
type: added
......@@ -49,6 +49,14 @@ The following resources are migrated to the target instance:
- parent epic ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297459))
- emoji award ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297466))
- events ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/297465))
- Milestones ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/292427))
- title
- description
- state (active / closed)
- start date
- due date
- created at
- updated at
Any other items are **not** migrated.
......
......@@ -22,6 +22,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MilestonesPipeline, context: context
expect_to_run_pipeline EE::BulkImports::Groups::Pipelines::EpicsPipeline, context: context
expect_to_run_pipeline EE::BulkImports::Groups::Pipelines::EpicAwardEmojiPipeline, context: context
......
# frozen_string_literal: true
module BulkImports
module Groups
module Graphql
module GetMilestonesQuery
extend self
def to_s
<<-'GRAPHQL'
query ($full_path: ID!, $cursor: String) {
group(fullPath: $full_path) {
milestones(first: 100, after: $cursor, includeDescendants: false) {
page_info: pageInfo {
end_cursor: endCursor
has_next_page: hasNextPage
}
nodes {
title
description
state
start_date: startDate
due_date: dueDate
created_at: createdAt
updated_at: updatedAt
}
}
}
}
GRAPHQL
end
def variables(context)
{
full_path: context.entity.source_full_path,
cursor: context.entity.next_page_for(:milestones)
}
end
def base_path
%w[data group milestones]
end
def data_path
base_path << 'nodes'
end
def page_info_path
base_path << 'page_info'
end
end
end
end
end
# frozen_string_literal: true
module BulkImports
module Groups
module Pipelines
class MilestonesPipeline
include Pipeline
extractor BulkImports::Common::Extractors::GraphqlExtractor,
query: BulkImports::Groups::Graphql::GetMilestonesQuery
transformer Common::Transformers::ProhibitedAttributesTransformer
def load(context, data)
return unless data
raise ::BulkImports::Pipeline::NotAllowedError unless authorized?
context.group.milestones.create!(data)
end
def after_run(extracted_data)
context.entity.update_tracker_for(
relation: :milestones,
has_next_page: extracted_data.has_next_page?,
next_page: extracted_data.next_page
)
if extracted_data.has_next_page?
run
end
end
private
def authorized?
context.current_user.can?(:admin_milestone, context.group)
end
end
end
end
end
......@@ -24,7 +24,8 @@ module BulkImports
BulkImports::Groups::Pipelines::GroupPipeline,
BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
BulkImports::Groups::Pipelines::MembersPipeline,
BulkImports::Groups::Pipelines::LabelsPipeline
BulkImports::Groups::Pipelines::LabelsPipeline,
BulkImports::Groups::Pipelines::MilestonesPipeline
]
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Groups::Graphql::GetMilestonesQuery do
it 'has a valid query' do
entity = create(:bulk_import_entity)
context = BulkImports::Pipeline::Context.new(entity)
query = GraphQL::Query.new(
GitlabSchema,
described_class.to_s,
variables: described_class.variables(context)
)
result = GitlabSchema.static_validator.validate(query)
expect(result[:errors]).to be_empty
end
describe '#data_path' do
it 'returns data path' do
expected = %w[data group milestones nodes]
expect(described_class.data_path).to eq(expected)
end
end
describe '#page_info_path' do
it 'returns pagination information path' do
expected = %w[data group milestones page_info]
expect(described_class.page_info_path).to eq(expected)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:cursor) { 'cursor' }
let_it_be(:timestamp) { Time.new(2020, 01, 01).utc }
let_it_be(:bulk_import) { create(:bulk_import, user: user) }
let(:entity) do
create(
:bulk_import_entity,
bulk_import: bulk_import,
source_full_path: 'source/full/path',
destination_name: 'My Destination Group',
destination_namespace: group.full_path,
group: group
)
end
let(:context) { BulkImports::Pipeline::Context.new(entity) }
subject { described_class.new(context) }
def milestone_data(title)
{
'title' => title,
'description' => 'desc',
'state' => 'closed',
'start_date' => '2020-10-21',
'due_date' => '2020-10-22',
'created_at' => timestamp.to_s,
'updated_at' => timestamp.to_s
}
end
def extracted_data(title:, has_next_page:, cursor: nil)
page_info = {
'end_cursor' => cursor,
'has_next_page' => has_next_page
}
BulkImports::Pipeline::ExtractedData.new(data: [milestone_data(title)], page_info: page_info)
end
before do
group.add_owner(user)
end
describe '#run' do
it 'imports group milestones' do
first_page = extracted_data(title: 'milestone1', has_next_page: true, cursor: cursor)
last_page = extracted_data(title: 'milestone2', has_next_page: false)
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
allow(extractor)
.to receive(:extract)
.and_return(first_page, last_page)
end
expect { subject.run }.to change(Milestone, :count).by(2)
expect(group.milestones.pluck(:title)).to contain_exactly('milestone1', 'milestone2')
milestone = group.milestones.last
expect(milestone.description).to eq('desc')
expect(milestone.state).to eq('closed')
expect(milestone.start_date.to_s).to eq('2020-10-21')
expect(milestone.due_date.to_s).to eq('2020-10-22')
expect(milestone.created_at).to eq(timestamp)
expect(milestone.updated_at).to eq(timestamp)
end
end
describe '#after_run' do
context 'when extracted data has next page' do
it 'updates tracker information and runs pipeline again' do
data = extracted_data(title: 'milestone', has_next_page: true, cursor: cursor)
expect(subject).to receive(:run)
subject.after_run(data)
tracker = entity.trackers.find_by(relation: :milestones)
expect(tracker.has_next_page).to eq(true)
expect(tracker.next_page).to eq(cursor)
end
end
context 'when extracted data has no next page' do
it 'updates tracker information and does not run pipeline' do
data = extracted_data(title: 'milestone', has_next_page: false)
expect(subject).not_to receive(:run)
subject.after_run(data)
tracker = entity.trackers.find_by(relation: :milestones)
expect(tracker.has_next_page).to eq(false)
expect(tracker.next_page).to be_nil
end
end
end
describe '#load' do
it 'creates the milestone' do
data = milestone_data('milestone')
expect { subject.load(context, data) }.to change(Milestone, :count).by(1)
end
context 'when user is not authorized to create the milestone' do
before do
allow(user).to receive(:can?).with(:admin_milestone, group).and_return(false)
end
it 'raises NotAllowedError' do
data = extracted_data(title: 'milestone', has_next_page: false)
expect { subject.load(context, data) }.to raise_error(::BulkImports::Pipeline::NotAllowedError)
end
end
end
describe 'pipeline parts' do
it { expect(described_class).to include_module(BulkImports::Pipeline) }
it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
it 'has extractors' do
expect(described_class.get_extractor)
.to eq(
klass: BulkImports::Common::Extractors::GraphqlExtractor,
options: {
query: BulkImports::Groups::Graphql::GetMilestonesQuery
}
)
end
it 'has transformers' do
expect(described_class.transformers)
.to contain_exactly(
{ klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil }
)
end
end
end
......@@ -22,6 +22,7 @@ RSpec.describe BulkImports::Importers::GroupImporter do
expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::MilestonesPipeline, context: context
if Gitlab.ee?
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context)
......
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