1# frozen_string_literal: true
2
3module MergeRequests
4  class CreateFromIssueService < MergeRequests::CreateService
5    # TODO: This constructor does not use the "params:" argument from the superclass,
6    #   but instead has a custom "mr_params:" argument. This is because historically,
7    #   prior to named arguments being introduced to the constructor, it never passed
8    #   along the third positional argument when calling `super`.
9    #   This should be changed, in order to be consistent (all subclasses should pass
10    #   along all of the arguments to the superclass, otherwise it is probably not an
11    #   "is a" relationship). However, we need to be sure that passing the params
12    #   argument to `super` (especially target_project_id) will not cause any unexpected
13    #   behavior in the superclass. Since the addition of the named arguments is
14    #   intended to be a low-risk pure refactor, we will defer this fix
15    #   to this follow-on issue:
16    #   https://gitlab.com/gitlab-org/gitlab/-/issues/328726
17    def initialize(project:, current_user:, mr_params: {})
18      # branch - the name of new branch
19      # ref    - the source of new branch.
20
21      @branch_name       = mr_params[:branch_name]
22      @issue_iid         = mr_params[:issue_iid]
23      @ref               = mr_params[:ref]
24      @target_project_id = mr_params[:target_project_id]
25
26      super(project: project, current_user: current_user)
27    end
28
29    def execute
30      return error('Project not found') if target_project.blank?
31      return error('Not allowed to create merge request') unless can_create_merge_request?
32      return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
33
34      result = ::Branches::CreateService.new(target_project, current_user).execute(branch_name, ref)
35      return result if result[:status] == :error
36
37      new_merge_request = create(merge_request)
38
39      if new_merge_request.valid?
40        merge_request_activity_counter.track_mr_create_from_issue(user: current_user)
41        SystemNoteService.new_merge_request(issue, project, current_user, new_merge_request)
42
43        success(new_merge_request)
44      else
45        SystemNoteService.new_issue_branch(issue, project, current_user, branch_name, branch_project: target_project)
46
47        error(new_merge_request.errors)
48      end
49    end
50
51    private
52
53    def can_create_merge_request?
54      can?(current_user, :create_merge_request_from, target_project)
55    end
56
57    # rubocop: disable CodeReuse/ActiveRecord
58    def issue
59      @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
60    end
61    # rubocop: enable CodeReuse/ActiveRecord
62
63    def branch_name
64      @branch ||= @branch_name || issue.to_branch_name
65    end
66
67    def ref
68      if valid_ref?
69        @ref
70      else
71        default_branch
72      end
73    end
74
75    def valid_ref?
76      ref_is_branch? || ref_is_tag?
77    end
78
79    def ref_is_branch?
80      target_project.repository.branch_exists?(@ref)
81    end
82
83    def ref_is_tag?
84      target_project.repository.tag_exists?(@ref)
85    end
86
87    def default_branch
88      target_project.default_branch_or_main
89    end
90
91    def merge_request
92      MergeRequests::BuildService.new(project: target_project, current_user: current_user, params: merge_request_params).execute
93    end
94
95    def merge_request_params
96      {
97        issue_iid: @issue_iid,
98        source_project_id: target_project.id,
99        source_branch: branch_name,
100        target_project_id: target_project.id,
101        target_branch: target_branch,
102        assignee_ids: [current_user.id]
103      }
104    end
105
106    def target_branch
107      if ref_is_branch?
108        @ref
109      else
110        default_branch
111      end
112    end
113
114    def success(merge_request)
115      super().merge(merge_request: merge_request)
116    end
117
118    def target_project
119      @target_project ||=
120        if @target_project_id.present?
121          project.forks.find_by_id(@target_project_id)
122        else
123          project
124        end
125    end
126  end
127end
128