summaryrefslogtreecommitdiff
path: root/app/services/projects/fork_service.rb
blob: 3e8d656370984aa3315d37cb06ab152258ba96c2 (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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# frozen_string_literal: true

module Projects
  class ForkService < BaseService
    def execute(fork_to_project = nil)
      forked_project = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project

      refresh_forks_count if forked_project&.saved?

      forked_project
    end

    def valid_fork_targets(options = {})
      @valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute(options)
    end

    def valid_fork_target?(namespace = target_namespace)
      return true if current_user.admin?

      valid_fork_targets.include?(namespace)
    end

    private

    def link_existing_project(fork_to_project)
      return if fork_to_project.forked?

      build_fork_network_member(fork_to_project)

      fork_to_project if link_fork_network(fork_to_project)
    end

    def fork_new_project
      new_project = CreateService.new(current_user, new_fork_params).execute
      return new_project unless new_project.persisted?

      new_project.project_feature.update!(
        @project.project_feature.slice(ProjectFeature::FEATURES.map { |f| "#{f}_access_level" })
      )

      new_project
    end

    def new_fork_params
      new_params = {
        forked_from_project:       @project,
        visibility_level:          target_visibility_level,
        description:               target_description,
        name:                      target_name,
        path:                      target_path,
        shared_runners_enabled:    @project.shared_runners_enabled,
        namespace_id:              target_namespace.id,
        fork_network:              fork_network,
        ci_config_path:            @project.ci_config_path,
        # We need to set ci_default_git_depth to 0 for the forked project when
        # @project.ci_default_git_depth is nil in order to keep the same behaviour
        # and not get ProjectCiCdSetting::DEFAULT_GIT_DEPTH set on create
        ci_cd_settings_attributes: { default_git_depth: @project.ci_default_git_depth || 0 },
        # We need to assign the fork network membership after the project has
        # been instantiated to avoid ActiveRecord trying to create it when
        # initializing the project, as that would cause a foreign key constraint
        # exception.
        relations_block:           -> (project) { build_fork_network_member(project) },
        skip_disk_validation:      skip_disk_validation,
        external_authorization_classification_label: @project.external_authorization_classification_label
      }

      if @project.avatar.present? && @project.avatar.image?
        new_params[:avatar] = @project.avatar
      end

      new_params[:mr_default_target_self] = target_mr_default_target_self unless target_mr_default_target_self.nil?

      new_params.merge!(@project.object_pool_params)

      new_params
    end

    def allowed_fork?
      current_user.can?(:fork_project, @project)
    end

    def fork_network
      @fork_network ||= @project.fork_network || @project.build_root_of_fork_network
    end

    def build_fork_network_member(fork_to_project)
      if allowed_fork?
        fork_to_project.build_fork_network_member(forked_from_project: @project,
                                                  fork_network: fork_network)
      else
        fork_to_project.errors.add(:forked_from_project_id, 'is forbidden')
      end
    end

    def link_fork_network(fork_to_project)
      return if fork_to_project.errors.any?

      fork_to_project.fork_network_member.save
    end

    def refresh_forks_count
      Projects::ForksCountService.new(@project).refresh_cache
    end

    def target_path
      @target_path ||= @params[:path] || @project.path
    end

    def target_name
      @target_name ||= @params[:name] || @project.name
    end

    def target_description
      @target_description ||= @params[:description] || @project.description
    end

    def target_namespace
      @target_namespace ||= @params[:namespace] || current_user.namespace
    end

    def skip_disk_validation
      @skip_disk_validation ||= @params[:skip_disk_validation] || false
    end

    def target_visibility_level
      target_level = [@project.visibility_level, target_namespace.visibility_level].min
      target_level = [target_level, Gitlab::VisibilityLevel.level_value(params[:visibility])].min if params.key?(:visibility)

      Gitlab::VisibilityLevel.closest_allowed_level(target_level)
    end

    def target_mr_default_target_self
      @target_mr_default_target_self ||= params[:mr_default_target_self]
    end
  end
end