project.rb 11.4 KB
Newer Older
1 2 3 4
module EE
  # Project EE mixin
  #
  # This module is intended to encapsulate EE-specific model logic
5
  # and be prepended in the `Project` model
6
  module Project
7
    extend ActiveSupport::Concern
8 9

    prepended do
10
      include IgnorableColumn
11 12 13
      include Elastic::ProjectsSearch
      prepend GeoAwareAvatar
      prepend ImportStatusStateMachine
14 15 16

      ignore_column :sync_time

17 18
      before_validation :mark_remote_mirrors_for_removal

19 20 21
      after_save :create_mirror_data, if: ->(project) { project.mirror? && project.mirror_changed? }
      after_save :destroy_mirror_data, if: ->(project) { !project.mirror? && project.mirror_changed? }

22 23 24 25 26
      after_update :remove_mirror_repository_reference,
        if: ->(project) { project.mirror? && project.import_url_updated? }

      belongs_to :mirror_user, foreign_key: 'mirror_user_id', class_name: 'User'

27
      has_one :mirror_data, dependent: :delete, autosave: true, class_name: 'ProjectMirrorData'
28 29 30 31 32 33 34 35 36 37
      has_one :push_rule, dependent: :destroy
      has_one :index_status, dependent: :destroy
      has_one :jenkins_service, dependent: :destroy
      has_one :jenkins_deprecated_service, dependent: :destroy

      has_many :approvers, as: :target, dependent: :destroy
      has_many :approver_groups, as: :target, dependent: :destroy
      has_many :audit_events, as: :entity, dependent: :destroy
      has_many :remote_mirrors, inverse_of: :project, dependent: :destroy
      has_many :path_locks, dependent: :destroy
38

39 40
      scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }

41
      scope :mirrors_to_sync, -> do
42 43
        mirror.joins(:mirror_data).where("next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')", Time.now)
          .order_by(:next_execution_timestamp).limit(::Gitlab::Mirror.available_capacity)
44 45 46
      end

      scope :stuck_mirrors, -> do
47 48
        mirror.joins(:mirror_data)
          .where("(import_status = 'started' AND project_mirror_data.last_update_started_at < :limit) OR (import_status = 'scheduled' AND project_mirror_data.last_update_scheduled_at < :limit)",
49 50 51
                { limit: 20.minutes.ago })
      end

52 53 54 55
      scope :mirror, -> { where(mirror: true) }
      scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
      scope :with_wiki_enabled, -> { with_feature_enabled(:wiki) }

56
      delegate :shared_runners_minutes, :shared_runners_seconds, :shared_runners_seconds_last_reset,
57 58 59 60
        to: :statistics, allow_nil: true

      delegate :actual_shared_runners_minutes_limit,
        :shared_runners_minutes_used?, to: :namespace
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 138 139

      validates :repository_size_limit,
        numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }

      validates :approvals_before_merge, numericality: true, allow_blank: true

      accepts_nested_attributes_for :remote_mirrors,
        allow_destroy: true,
        reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }

      with_options if: :mirror? do |project|
        project.validates :import_url, presence: true
        project.validates :mirror_user, presence: true
      end
    end

    module ClassMethods
      def search_by_visibility(level)
        where(visibility_level: ::Gitlab::VisibilityLevel.string_options[level])
      end
    end

    def mirror_updated?
      mirror? && self.mirror_last_update_at
    end

    def updating_mirror?
      return false unless mirror? && !empty_repo?
      return true if import_in_progress?

      self.mirror_data.next_execution_timestamp < Time.now
    end

    def mirror_last_update_status
      return unless mirror_updated?

      if self.mirror_last_update_at == self.mirror_last_successful_update_at
        :success
      else
        :failed
      end
    end

    def mirror_last_update_success?
      mirror_last_update_status == :success
    end

    def mirror_last_update_failed?
      mirror_last_update_status == :failed
    end

    def mirror_ever_updated_successfully?
      mirror_updated? && self.mirror_last_successful_update_at
    end

    def has_remote_mirror?
      remote_mirrors.enabled.exists?
    end

    def updating_remote_mirror?
      remote_mirrors.enabled.started.exists?
    end

    def update_remote_mirrors
      remote_mirrors.each(&:sync)
    end

    def mark_stuck_remote_mirrors_as_failed!
      remote_mirrors.stuck.update_all(
        update_status: :failed,
        last_error: 'The remote mirror took to long to complete.',
        last_update_at: Time.now
      )
    end

    def fetch_mirror
      return unless mirror?

      repository.fetch_upstream(self.import_url)
140 141 142 143 144 145 146 147 148
    end

    def shared_runners_available?
      super && !namespace.shared_runners_minutes_used?
    end

    def shared_runners_minutes_limit_enabled?
      !public? && shared_runners_enabled? && namespace.shared_runners_minutes_limit_enabled?
    end
149

150 151 152 153 154 155 156 157 158 159 160
    # Checks licensed feature availability if `feature` matches any
    # key on License::FEATURE_CODES. Otherwise, check feature availability
    # through ProjectFeature.
    def feature_available?(feature, user = nil)
      if License::FEATURE_CODES.key?(feature)
        licensed_feature_available?(feature)
      else
        super
      end
    end

161 162 163 164 165
    def service_desk_enabled
      ::EE::Gitlab::ServiceDesk.enabled?(project: self) && super
    end
    alias_method :service_desk_enabled?, :service_desk_enabled

166
    def service_desk_address
167
      return nil unless service_desk_enabled?
168

169 170
      config = ::Gitlab.config.incoming_email
      wildcard = ::Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER
171

172
      config.address&.gsub(wildcard, full_path)
173 174
    end

175 176 177 178 179 180 181 182 183
    def force_import_job!
      self.mirror_data.set_next_execution_to_now!
      UpdateAllMirrorsWorker.perform_async
    end

    def add_import_job
      if import? && !repository_exists?
        super
      elsif mirror?
184 185
        ::Gitlab::Mirror.increment_metric(:mirrors_scheduled, 'Mirrors scheduled count')

186 187 188 189
        RepositoryUpdateMirrorWorker.perform_async(self.id)
      end
    end

190
    def secret_variables_for(ref:, environment: nil)
191 192
      return super.where(environment_scope: '*') unless
        environment && feature_available?(:variable_environment_scope)
193

194
      query = super
195

196
      where = <<~SQL
197
        environment_scope IN (:wildcard, :environment_name) OR
198
          :environment_name LIKE
199 200 201 202
            REPLACE(REPLACE(REPLACE(environment_scope,
                                    :underscore, :escaped_underscore),
                            :percent, :escaped_percent),
                    :wildcard, :percent)
203 204 205
      SQL

      order = <<~SQL
206
        CASE environment_scope
207 208 209 210 211 212 213 214 215 216
          WHEN %{wildcard} THEN 0
          WHEN %{environment_name} THEN 2
          ELSE 1
        END
      SQL

      values = {
        wildcard: '*',
        environment_name: environment.name,
        percent: '%',
217
        escaped_percent: '\\%',
218 219 220 221 222 223 224 225 226 227
        underscore: '_',
        escaped_underscore: '\\_'
      }

      quoted_values =
        values.transform_values(&self.class.connection.method(:quote))

      query
        .where(where, values)
        .order(order % quoted_values) # `order` cannot escape for us!
228 229
    end

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
    def cache_has_external_issue_tracker
      super unless ::Gitlab::Geo.secondary?
    end

    def cache_has_external_wiki
      super unless ::Gitlab::Geo.secondary?
    end

    def execute_hooks(data, hooks_scope = :push_hooks)
      super

      if group
        group.hooks.send(hooks_scope).each do |hook|
          hook.async_execute(data, hooks_scope.to_s)
        end
      end
    end

    # No need to have a Kerberos Web url. Kerberos URL will be used only to
    # clone
    def kerberos_url_to_repo
      "#{::Gitlab.config.build_gitlab_kerberos_url + ::Gitlab::Application.routes.url_helpers.namespace_project_path(self.namespace, self)}.git"
    end

    def group_ldap_synced?
      if group
        group.ldap_synced?
      else
        false
      end
    end

    def reference_issue_tracker?
      default_issues_tracker? || jira_tracker_active?
    end

    def approver_ids=(value)
      value.split(",").map(&:strip).each do |user_id|
        approvers.find_or_create_by(user_id: user_id, target_id: id)
      end
    end

    def approver_group_ids=(value)
      value.split(",").map(&:strip).each do |group_id|
        approver_groups.find_or_initialize_by(group_id: group_id, target_id: id)
      end
    end

    def find_path_lock(path, exact_match: false, downstream: false)
      @path_lock_finder ||= ::Gitlab::PathLocksFinder.new(self)
      @path_lock_finder.find(path, exact_match: exact_match, downstream: downstream)
    end

    def merge_method
      if self.merge_requests_ff_only_enabled
        :ff
      elsif self.merge_requests_rebase_enabled
        :rebase_merge
      else
        :merge
      end
    end

    def merge_method=(method)
      case method.to_s
      when "ff"
        self.merge_requests_ff_only_enabled = true
        self.merge_requests_rebase_enabled = true
      when "rebase_merge"
        self.merge_requests_ff_only_enabled = false
        self.merge_requests_rebase_enabled = true
      when "merge"
        self.merge_requests_ff_only_enabled = false
        self.merge_requests_rebase_enabled = false
      end
    end

    def ff_merge_must_be_possible?
      self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
    end

    def import_url_updated?
      # check if import_url has been updated and it's not just the first assignment
      import_url_changed? && changes['import_url'].first
    end

    def remove_mirror_repository_reference
      repository.remove_remote(Repository::MIRROR_REMOTE)
    end

    def import_url_availability
      if remote_mirrors.find_by(url: import_url)
        errors.add(:import_url, 'is already in use by a remote mirror')
      end
    end

    def mark_remote_mirrors_for_removal
      remote_mirrors.each(&:mark_for_delete_if_blank_url)
    end

    def change_repository_storage(new_repository_storage_key)
      return if repository_read_only?
      return if repository_storage == new_repository_storage_key

      raise ArgumentError unless ::Gitlab.config.repositories.storages.keys.include?(new_repository_storage_key)

      run_after_commit { ProjectUpdateRepositoryStorageWorker.perform_async(id, new_repository_storage_key) }
      self.repository_read_only = true
    end

    def repository_and_lfs_size
      statistics.total_repository_size
    end

    def above_size_limit?
      return false unless size_limit_enabled?

      repository_and_lfs_size > actual_size_limit
    end

    def size_to_remove
      repository_and_lfs_size - actual_size_limit
    end

    def actual_size_limit
      return namespace.actual_size_limit if repository_size_limit.nil?

      repository_size_limit
    end

    def size_limit_enabled?
      actual_size_limit != 0
    end

    def changes_will_exceed_size_limit?(size_in_bytes)
      size_limit_enabled? &&
        (size_in_bytes > actual_size_limit ||
         size_in_bytes + repository_and_lfs_size > actual_size_limit)
    end

    def remove_import_data
      super unless mirror?
    end

374
    private
375

376
    def licensed_feature_available?(feature)
377
      globally_available = License.feature_available?(feature)
378 379 380 381 382 383 384 385 386

      if current_application_settings.should_check_namespace_plan?
        globally_available &&
          (public? && namespace.public? || namespace.feature_available?(feature))
      else
        globally_available
      end
    end

387 388 389
    def destroy_mirror_data
      mirror_data.destroy
    end
390 391
  end
end