summaryrefslogtreecommitdiff
path: root/app/services/issues/clone_service.rb
blob: 07dd9a98f898cd6c5c1977a5a74e91bbe8760c7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# frozen_string_literal: true

module Issues
  class CloneService < Issuable::Clone::BaseService
    CloneError = Class.new(StandardError)

    def execute(issue, target_project, with_notes: false)
      @target_project = target_project
      @with_notes = with_notes

      verify_can_clone_issue!(issue, target_project)

      super(issue, target_project)

      notify_participants

      queue_copy_designs

      new_entity
    end

    private

    attr_reader :target_project
    attr_reader :with_notes

    def verify_can_clone_issue!(issue, target_project)
      unless issue.supports_move_and_clone?
        raise CloneError, s_('CloneIssue|Cannot clone issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type }
      end

      unless issue.can_clone?(current_user, target_project)
        raise CloneError, s_('CloneIssue|Cannot clone issue due to insufficient permissions!')
      end

      if target_project.pending_delete?
        raise CloneError, s_('CloneIssue|Cannot clone issue to target project as it is pending deletion.')
      end
    end

    def update_new_entity
      # we don't call `super` because we want to be able to decide whether or not to copy all comments over.
      update_new_entity_description

      if with_notes
        copy_notes
        copy_resource_events
      end
    end

    def update_old_entity
      # no-op
      # The base_service closes the old issue, we don't want that, so we override here so nothing happens.
    end

    def create_new_entity
      new_params = {
        id: nil,
        iid: nil,
        relative_position: relative_position,
        project: target_project,
        author: current_user,
        assignee_ids: original_entity.assignee_ids
      }

      new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
      new_params = new_params.merge(rewritten_old_entity_attributes)
      new_params.delete(:created_at)
      new_params.delete(:updated_at)

      # spam checking is not necessary, as no new content is being created. Passing nil for
      # spam_params will cause SpamActionService to skip checking and return a success response.
      spam_params = nil

      # Skip creation of system notes for existing attributes of the issue when cloning with notes.
      # The system notes of the old issue are copied over so we don't want to end up with duplicate notes.
      # When cloning without notes, we want to generate system notes for the attributes that were copied.
      CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: with_notes)
    end

    def queue_copy_designs
      return unless original_entity.designs.present?

      response = DesignManagement::CopyDesignCollection::QueueService.new(
        current_user,
        original_entity,
        new_entity
      ).execute

      log_error(response.message) if response.error?
    end

    def notify_participants
      notification_service.async.issue_cloned(original_entity, new_entity, current_user)
    end

    def add_note_from
      SystemNoteService.noteable_cloned(
        new_entity,
        target_project,
        original_entity,
        current_user,
        direction: :from,
        created_at: new_entity.created_at
      )
    end

    def add_note_to
      SystemNoteService.noteable_cloned(original_entity, old_project,
        new_entity, current_user,
        direction: :to)
    end
  end
end

Issues::CloneService.prepend_mod_with('Issues::CloneService')