GitLab steht Mittwoch, den 23. September, zwischen 10:00 und 12:00 Uhr aufgrund von Wartungsarbeiten nicht zur Verfügung.

Commit ec3712c2 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-3777-promote-to-epic' into 'master'

Refactoring Issues::MoveService (port of promote epics)

See merge request gitlab-org/gitlab-ce!22766
parents 8f60a8ba 4af1712d
# frozen_string_literal: true
module Issuable
module Clone
class AttributesRewriter < ::Issuable::Clone::BaseService
def initialize(current_user, original_entity, new_entity)
@current_user = current_user
@original_entity = original_entity
@new_entity = new_entity
end
def execute
new_entity.update(milestone: cloneable_milestone, labels: cloneable_labels)
copy_resource_label_events
end
private
def cloneable_milestone
title = original_entity.milestone&.title
return unless title
params = { title: title, project_ids: new_entity.project&.id, group_ids: group&.id }
milestones = MilestonesFinder.new(params).execute
milestones.first
end
def cloneable_labels
params = {
project_id: new_entity.project&.id,
group_id: group&.id,
title: original_entity.labels.select(:title),
include_ancestor_groups: true
}
params[:only_group_labels] = true if new_parent.is_a?(Group)
LabelsFinder.new(current_user, params).execute
end
def copy_resource_label_events
original_entity.resource_label_events.find_in_batches do |batch|
events = batch.map do |event|
entity_key = new_entity.is_a?(Issue) ? 'issue_id' : 'epic_id'
# rubocop: disable CodeReuse/ActiveRecord
event.attributes
.except('id', 'reference', 'reference_html')
.merge(entity_key => new_entity.id, 'action' => ResourceLabelEvent.actions[event.action])
# rubocop: enable CodeReuse/ActiveRecord
end
Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events)
end
end
def entity_key
new_entity.class.name.parameterize('_').foreign_key
end
end
end
end
# frozen_string_literal: true
module Issuable
module Clone
class BaseService < IssuableBaseService
attr_reader :original_entity, :new_entity
alias_method :old_project, :project
def execute(original_entity, new_project = nil)
@original_entity = original_entity
# Using transaction because of a high resources footprint
# on rewriting notes (unfolding references)
#
ActiveRecord::Base.transaction do
@new_entity = create_new_entity
update_new_entity
update_old_entity
create_notes
end
end
private
def update_new_entity
rewriters = [ContentRewriter, AttributesRewriter]
rewriters.each do |rewriter|
rewriter.new(current_user, original_entity, new_entity).execute
end
end
def update_old_entity
close_issue
end
def create_notes
add_note_from
add_note_to
end
def close_issue
close_service = Issues::CloseService.new(old_project, current_user)
close_service.execute(original_entity, notifications: false, system_note: false)
end
def new_parent
new_entity.project ? new_entity.project : new_entity.group
end
def group
if new_entity.project&.group && current_user.can?(:read_group, new_entity.project.group)
new_entity.project.group
end
end
end
end
end
# frozen_string_literal: true
module Issuable
module Clone
class ContentRewriter < ::Issuable::Clone::BaseService
def initialize(current_user, original_entity, new_entity)
@current_user = current_user
@original_entity = original_entity
@new_entity = new_entity
@project = original_entity.project
end
def execute
rewrite_description
rewrite_award_emoji(original_entity, new_entity)
rewrite_notes
end
private
def rewrite_description
new_entity.update(description: rewrite_content(original_entity.description))
end
def rewrite_notes
original_entity.notes_with_associations.find_each do |note|
new_note = note.dup
new_params = {
project: new_entity.project, noteable: new_entity,
note: rewrite_content(new_note.note),
created_at: note.created_at,
updated_at: note.updated_at
}
if note.system_note_metadata
new_params[:system_note_metadata] = note.system_note_metadata.dup
end
new_note.update(new_params)
rewrite_award_emoji(note, new_note)
end
end
def rewrite_content(content)
return unless content
rewriters = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter]
rewriters.inject(content) do |text, klass|
rewriter = klass.new(text, old_project, current_user)
rewriter.rewrite(new_parent)
end
end
def rewrite_award_emoji(old_awardable, new_awardable)
old_awardable.award_emoji.each do |award|
new_award = award.dup
new_award.awardable = new_awardable
new_award.save
end
end
end
end
end
# frozen_string_literal: true
module Issues
class MoveService < Issues::BaseService
class MoveService < Issuable::Clone::BaseService
MoveError = Class.new(StandardError)
def execute(issue, new_project)
@old_issue = issue
@old_project = @project
@new_project = new_project
def execute(issue, target_project)
@target_project = target_project
unless issue.can_move?(current_user, new_project)
unless issue.can_move?(current_user, @target_project)
raise MoveError, 'Cannot move issue due to insufficient permissions!'
end
if @project == new_project
if @project == @target_project
raise MoveError, 'Cannot move issue to project it originates from!'
end
# Using transaction because of a high resources footprint
# on rewriting notes (unfolding references)
#
ActiveRecord::Base.transaction do
@new_issue = create_new_issue
update_new_issue
update_old_issue
end
super
notify_participants
@new_issue
new_entity
end
private
def update_new_issue
rewrite_notes
copy_resource_label_events
rewrite_issue_award_emoji
add_note_moved_from
end
def update_old_entity
super
def update_old_issue
add_note_moved_to
close_issue
mark_as_moved
end
def create_new_issue
new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
milestone_id: cloneable_milestone_id,
project: @new_project, author: @old_issue.author,
description: rewrite_content(@old_issue.description),
assignee_ids: @old_issue.assignee_ids }
new_params = @old_issue.serializable_hash.symbolize_keys.merge(new_params)
CreateService.new(@new_project, @current_user, new_params).execute
end
# rubocop: disable CodeReuse/ActiveRecord
def cloneable_label_ids
params = {
project_id: @new_project.id,
title: @old_issue.labels.pluck(:title),
include_ancestor_groups: true
}
def create_new_entity
new_params = {
id: nil,
iid: nil,
project: @target_project,
author: original_entity.author,
assignee_ids: original_entity.assignee_ids
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
CreateService.new(@target_project, @current_user, new_params).execute
end
# rubocop: enable CodeReuse/ActiveRecord
def cloneable_milestone_id
title = @old_issue.milestone&.title
return unless title
if @new_project.group && can?(current_user, :read_group, @new_project.group)
group_id = @new_project.group.id
end
params =
{ title: title, project_ids: @new_project.id, group_ids: group_id }
milestones = MilestonesFinder.new(params).execute
milestones.first&.id
end
def rewrite_notes
@old_issue.notes_with_associations.find_each do |note|
new_note = note.dup
new_params = { project: @new_project, noteable: @new_issue,
note: rewrite_content(new_note.note),
created_at: note.created_at,
updated_at: note.updated_at }
new_note.update(new_params)
rewrite_award_emoji(note, new_note)
end
end
# rubocop: disable CodeReuse/ActiveRecord
def copy_resource_label_events
@old_issue.resource_label_events.find_in_batches do |batch|
events = batch.map do |event|
event.attributes
.except('id', 'reference', 'reference_html')
.merge('issue_id' => @new_issue.id, 'action' => ResourceLabelEvent.actions[event.action])
end
Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def rewrite_issue_award_emoji
rewrite_award_emoji(@old_issue, @new_issue)
end
def rewrite_award_emoji(old_awardable, new_awardable)
old_awardable.award_emoji.each do |award|
new_award = award.dup
new_award.awardable = new_awardable
new_award.save
end
end
def rewrite_content(content)
return unless content
rewriters = [Gitlab::Gfm::ReferenceRewriter,
Gitlab::Gfm::UploadsRewriter]
rewriters.inject(content) do |text, klass|
rewriter = klass.new(text, @old_project, @current_user)
rewriter.rewrite(@new_project)
end
def mark_as_moved
original_entity.update(moved_to: new_entity)
end
def close_issue
close_service = CloseService.new(@old_project, @current_user)
close_service.execute(@old_issue, notifications: false, system_note: false)
def notify_participants
notification_service.async.issue_moved(original_entity, new_entity, @current_user)
end
def add_note_moved_from
SystemNoteService.noteable_moved(@new_issue, @new_project,
@old_issue, @current_user,
def add_note_from
SystemNoteService.noteable_moved(new_entity, @target_project,
original_entity, current_user,
direction: :from)
end
def add_note_moved_to
SystemNoteService.noteable_moved(@old_issue, @old_project,
@new_issue, @current_user,
def add_note_to
SystemNoteService.noteable_moved(original_entity, old_project,
new_entity, current_user,
direction: :to)
end
def mark_as_moved
@old_issue.update(moved_to: @new_issue)
end
def notify_participants
notification_service.async.issue_moved(@old_issue, @new_issue, @current_user)
end
end
end
......@@ -149,9 +149,9 @@ class FileUploader < GitlabUploader
# return a new uploader with a file copy on another project
def self.copy_to(uploader, to_project)
moved = uploader.dup.tap do |u|
u.model = to_project
end
moved = self.new(to_project)
moved.object_store = uploader.object_store
moved.filename = uploader.filename
moved.copy_file(uploader.file)
moved
......
......@@ -31,19 +31,19 @@ module Gitlab
class ReferenceRewriter
RewriteError = Class.new(StandardError)
def initialize(text, source_project, current_user)
def initialize(text, source_parent, current_user)
@text = text
@source_project = source_project
@source_parent = source_parent
@current_user = current_user
@original_html = markdown(text)
@pattern = Gitlab::ReferenceExtractor.references_pattern
end
def rewrite(target_project)
def rewrite(target_parent)
return @text unless needs_rewrite?
@text.gsub(@pattern) do |reference|
unfold_reference(reference, Regexp.last_match, target_project)
unfold_reference(reference, Regexp.last_match, target_parent)
end
end
......@@ -53,14 +53,14 @@ module Gitlab
private
def unfold_reference(reference, match, target_project)
def unfold_reference(reference, match, target_parent)
before = @text[0...match.begin(0)]
after = @text[match.end(0)..-1]
referable = find_referable(reference)
return reference unless referable
cross_reference = build_cross_reference(referable, target_project)
cross_reference = build_cross_reference(referable, target_parent)
return reference if reference == cross_reference
if cross_reference.nil?
......@@ -72,17 +72,17 @@ module Gitlab
end
def find_referable(reference)
extractor = Gitlab::ReferenceExtractor.new(@source_project,
extractor = Gitlab::ReferenceExtractor.new(@source_parent,
@current_user)
extractor.analyze(reference)
extractor.all.first
end
def build_cross_reference(referable, target_project)
def build_cross_reference(referable, target_parent)
if referable.respond_to?(:project)
referable.to_reference(target_project)
referable.to_reference(target_parent)
else
referable.to_reference(@source_project, target_project: target_project)
referable.to_reference(@source_parent, target_project: target_parent)
end
end
......@@ -91,7 +91,7 @@ module Gitlab
end
def markdown(text)
Banzai.render(text, project: @source_project, no_original_data: true)
Banzai.render(text, project: @source_parent, no_original_data: true)
end
end
end
......
......@@ -16,14 +16,15 @@ module Gitlab
@pattern = FileUploader::MARKDOWN_PATTERN
end
def rewrite(target_project)
def rewrite(target_parent)
return @text unless needs_rewrite?
@text.gsub(@pattern) do |markdown|
file = find_file(@source_project, $~[:secret], $~[:file])
break markdown unless file.try(:exists?)
moved = FileUploader.copy_to(file, target_project)
klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
moved = klass.copy_to(file, target_parent)
moved.markdown_link
end
end
......
......@@ -2,13 +2,14 @@ module Gitlab
module QuickActions
class CommandDefinition
attr_accessor :name, :aliases, :description, :explanation, :params,
:condition_block, :parse_params_block, :action_block
:condition_block, :parse_params_block, :action_block, :warning
def initialize(name, attributes = {})
@name = name
@aliases = attributes[:aliases] || []
@description = attributes[:description] || ''
@warning = attributes[:warning] || ''
@explanation = attributes[:explanation] || ''
@params = attributes[:params] || []
@condition_block = attributes[:condition_block]
......@@ -33,11 +34,13 @@ module Gitlab
def explain(context, arg)
return unless available?(context)
if explanation.respond_to?(:call)
execute_block(explanation, context, arg)
else
explanation
end
message = if explanation.respond_to?(:call)
execute_block(explanation, context, arg)
else
explanation
end
warning.empty? ? message : "#{message} (#{warning})"
end
def execute(context, arg)
......@@ -61,6 +64,7 @@ module Gitlab
name: name,
aliases: aliases,
description: desc,
warning: warning,
params: prms
}
end
......
......@@ -31,6 +31,10 @@ module Gitlab
@description = block_given? ? block : text
end
def warning(message = '')
@warning = message
end
# Allows to define params for the next quick action.
# These params are shown in the autocomplete menu.
#
......@@ -133,6 +137,7 @@ module Gitlab
name,
aliases: aliases,
description: @description,
warning: @warning,
explanation: @explanation,
params: @params,
condition_block: @condition_block,
......@@ -150,6 +155,7 @@ module Gitlab
@explanation = nil
@params = nil
@condition_block = nil
@warning = nil
@parse_params_block = nil
end
end
......
......@@ -210,6 +210,19 @@ describe Gitlab::QuickActions::CommandDefinition do
end
end
context 'when warning is set' do
before do
subject.explanation = 'Explanation'
subject.warning = 'dangerous!'
end
it 'returns this static string' do
result = subject.explain({}, nil)
expect(result).to eq 'Explanation (dangerous!)'
end
end
context 'when the explanation is dynamic' do
before do
subject.explanation = proc { |arg| "Dynamic #{arg}" }
......
......@@ -12,6 +12,7 @@ describe Gitlab::QuickActions::Dsl do
params 'The first argument'
explanation 'Static explanation'
warning 'Possible problem!'
command :explanation_with_aliases, :once, :first do |arg|
arg
end
......@@ -64,6 +65,7 @@ describe Gitlab::QuickActions::Dsl do
expect(no_args_def.condition_block).to be_nil
expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(no_args_def.parse_params_block).to be_nil
expect(no_args_def.warning).to eq('')
expect(explanation_with_aliases_def.name).to eq(:explanation_with_aliases)
expect(explanation_with_aliases_def.aliases).to eq([:once, :first])
......@@ -73,6 +75,7 @@ describe Gitlab::QuickActions::Dsl do
expect(explanation_with_aliases_def.condition_block).to be_nil
expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
expect(explanation_with_aliases_def.parse_params_block).to be_nil
expect(explanation_with_aliases_def.warning).to eq('Possible problem!')
expect(dynamic_description_def.name).to eq(:dynamic_description)
expect(dynamic_description_def.aliases).to eq([])
......@@ -82,6 +85,7 @@ describe Gitlab::QuickActions::Dsl do
expect(dynamic_description_def.condition_block).to be_nil
expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
expect(dynamic_description_def.parse_params_block).to be_nil
expect(dynamic_description_def.warning).to eq('')
expect(cc_def.name).to eq(:cc)
expect(cc_def.aliases).to eq([])
......@@ -91,6 +95,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cc_def.condition_block).to be_nil
expect(cc_def.action_block).to be_nil
expect(cc_def.parse_params_block).to be_nil
expect(cc_def.warning).to eq('')
expect(cond_action_def.name).to eq(:cond_action)
expect(cond_action_def.aliases).to eq([])
......@@ -100,6 +105,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
expect(cond_action_def.action_block).to be_a_kind_of(Proc)
expect(cond_action_def.parse_params_block).to be_nil
expect(cond_action_def.warning).to eq('')
expect(with_params_parsing_def.name).to eq(:with_params_parsing)
expect(with_params_parsing_def.aliases).to eq([])
......@@ -109,6 +115,7 @@ describe Gitlab::QuickActions::Dsl do
expect(with_params_parsing_def.condition_block).to be_nil
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.warning).to eq('')
expect(substitution_def.name).to eq(:something)
expect(substitution_def.aliases).to eq([])
......@@ -118,6 +125,7 @@ describe Gitlab::QuickActions::Dsl do
expect(substitution_def.condition_block).to be_nil
expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
expect(substitution_def.parse_params_block).to be_nil
expect(substitution_def.warning).to eq('')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Issuable::Clone::AttributesRewriter do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project1) { create(:project, :public, group: group) }
let(:project2) { create(:project, :public, group: group) }
let(:original_issue) { create(:issue, project: project1) }
let(:new_issue) { create(:issue, project: project2) }
subject { described_class.new(user, original_issue, new_issue) }
context 'setting labels' do
it 'sets labels present in the new project and group labels' do
project1_label_1 = create(:label, title: 'label1', project: project1)
project1_label_2 = create(:label, title: 'label2', project: project1)
project2_label_1 = create(:label, title: 'label1', project: project2)
group_label = create(:group_label, title: 'group_label', group: group)
create(:label, title: 'label3', project: project2)
original_issue.update(labels: [project1_label_1, project1_label_2, group_label])
subject.execute
expect(new_issue.reload.labels).to match_array([project2_label_1, group_label])
end
it 'does not set any labels when not used on the original issue' do
subject.execute
expect(new_issue.reload.labels).to be_empty
end
it 'copies the resource label events' do
resource_label_events = create_list(:resource_label_event, 2, issue: original_issue)
subject.execute
expected = resource_label_events.map(&:label_id)
expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected)
end
end
context 'setting milestones' do
it 'sets milestone to nil when old issue milestone is not in the new project' do
milestone = create(:milestone, title: 'milestone', project: project1)
original_issue.update(milestone: milestone)
subject.execute
expect(new_issue.reload.milestone).to be_nil
end
it 'copies the milestone when old issue milestone title is in the new project' do
milestone_project1 = create(:milestone, title: 'milestone', project: project1)
milestone_project2 = create(:milestone, title: 'milestone', project: project2)
original_issue.update(milestone: milestone_project1)