project.rb 58.6 KB
Newer Older
1 2
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
3
class Project < ActiveRecord::Base
4
  include Gitlab::ConfigHelper
5
  include Gitlab::ShellAdapter
6
  include Gitlab::VisibilityLevel
7
  include AccessRequestable
8
  include Avatarable
9
  include CacheMarkdownField
10 11
  include Referable
  include Sortable
12
  include AfterCommitQueue
13
  include CaseSensitivity
14
  include TokenAuthenticatable
15
  include ValidAttribute
16
  include ProjectFeaturesCompatibility
17
  include SelectForProjectAuthorization
18
  include Presentable
19
  include Routable
20
  include GroupDescendant
21
  include Gitlab::SQL::Pattern
22
  include DeploymentPlatform
23
  include ::Gitlab::Utils::StrongMemoize
Robert Speicher's avatar
Robert Speicher committed
24

25
  extend Gitlab::ConfigHelper
26

27
  BoardLimitExceeded = Class.new(StandardError)
28

29
  NUMBER_OF_PERMITTED_BOARDS = 1
Douwe Maan's avatar
Douwe Maan committed
30
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
31 32
  # Hashed Storage versions handle rolling out new storage to project and dependents models:
  # nil: legacy
33 34 35
  # 1: repository
  # 2: attachments
  LATEST_STORAGE_VERSION = 2
36 37 38 39
  HASHED_STORAGE_FEATURES = {
    repository: 1,
    attachments: 2
  }.freeze
Jared Szechy's avatar
Jared Szechy committed
40

41 42 43
  # Valids ports to import from
  VALID_IMPORT_PORTS = [22, 80, 443].freeze

44 45
  cache_markdown_field :description, pipeline: :description

46 47
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
48
                                                       allow_nil: true
49

50
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
51

52
  default_value_for :archived, false
53
  default_value_for :visibility_level, gitlab_config_features.visibility_level
54
  default_value_for :resolve_outdated_diff_discussions, false
55
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
56 57
  default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
58 59 60 61 62
  default_value_for :issues_enabled, gitlab_config_features.issues
  default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
  default_value_for :builds_enabled, gitlab_config_features.builds
  default_value_for :wiki_enabled, gitlab_config_features.wiki
  default_value_for :snippets_enabled, gitlab_config_features.snippets
63
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
64

65 66
  add_authentication_token_field :runners_token
  before_save :ensure_runners_token
67

68 69
  after_save :update_project_statistics, if: :namespace_id_changed?
  after_create :create_project_feature, unless: :project_feature
70
  after_create :set_last_activity_at
71
  after_create :set_last_repository_updated_at
72
  after_update :update_forks_visibility_level
73

74
  before_destroy :remove_private_deploy_keys
75
  after_destroy -> { run_after_commit { remove_pages } }
76
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
77

78 79
  after_validation :check_pending_delete

80
  # Storage specific hooks
81
  after_initialize :use_hashed_storage
82
  after_create :check_repository_absence!
83 84
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
85

86
  acts_as_taggable
87

88
  attr_accessor :old_path_with_namespace
89
  attr_accessor :template_name
90
  attr_writer :pipeline_status
91
  attr_accessor :skip_disk_validation
92

93 94
  alias_attribute :title, :name

95
  # Relations
96
  belongs_to :creator, class_name: 'User'
97
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
98
  belongs_to :namespace
99 100
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
101

102
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
103
  has_many :boards, before_add: :validate_board_limit
104

105
  # Project services
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
  has_one :campfire_service
  has_one :drone_ci_service
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
  has_one :hipchat_service
  has_one :flowdock_service
  has_one :assembla_service
  has_one :asana_service
  has_one :gemnasium_service
  has_one :mattermost_slash_commands_service
  has_one :mattermost_service
  has_one :slack_slash_commands_service
  has_one :slack_service
  has_one :buildkite_service
  has_one :bamboo_service
  has_one :teamcity_service
  has_one :pushover_service
  has_one :jira_service
  has_one :redmine_service
  has_one :custom_issue_tracker_service
  has_one :bugzilla_service
  has_one :gitlab_issue_tracker_service, inverse_of: :project
  has_one :external_wiki_service
  has_one :kubernetes_service, inverse_of: :project
  has_one :prometheus_service, inverse_of: :project
  has_one :mock_ci_service
  has_one :mock_deployment_service
  has_one :mock_monitoring_service
  has_one :microsoft_teams_service
137
  has_one :packagist_service
138

139
  # TODO: replace these relations with the fork network versions
140
  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
141 142 143 144
  has_one  :forked_from_project,  through:   :forked_project_link

  has_many :forked_project_links, foreign_key: "forked_from_project_id"
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
145 146 147 148 149 150 151 152
  # TODO: replace these relations with the fork network versions

  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
  has_one :fork_network, through: :fork_network_member
153

154
  # Merge Requests for target project should be removed with it
155
  has_many :merge_requests, foreign_key: 'target_project_id'
156
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
157 158 159 160 161 162 163 164 165 166
  has_many :issues
  has_many :labels, class_name: 'ProjectLabel'
  has_many :services
  has_many :events
  has_many :milestones
  has_many :notes
  has_many :snippets, class_name: 'ProjectSnippet'
  has_many :hooks, class_name: 'ProjectHook'
  has_many :protected_branches
  has_many :protected_tags
167

168
  has_many :project_authorizations
169
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
170
  has_many :project_members, -> { where(requested_at: nil) },
171
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
172

173
  alias_method :members, :project_members
174 175
  has_many :users, through: :project_members

176
  has_many :requesters, -> { where.not(requested_at: nil) },
177
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
178
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
179

180
  has_many :deploy_keys_projects
181
  has_many :deploy_keys, through: :deploy_keys_projects
182
  has_many :users_star_projects
Ciro Santilli's avatar
Ciro Santilli committed
183
  has_many :starrers, through: :users_star_projects, source: :user
184
  has_many :releases
185
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Marin Jankovski's avatar
Marin Jankovski committed
186
  has_many :lfs_objects, through: :lfs_objects_projects
187
  has_many :lfs_file_locks
188
  has_many :project_group_links
189
  has_many :invited_groups, through: :project_group_links, source: :group
190 191
  has_many :pages_domains
  has_many :todos
192
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
193

194 195
  has_many :internal_ids

196
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
197
  has_one :project_feature, inverse_of: :project
198
  has_one :statistics, class_name: 'ProjectStatistics'
199

Shinya Maeda's avatar
Shinya Maeda committed
200
  has_one :cluster_project, class_name: 'Clusters::Project'
201
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
202

203 204 205
  # Container repositories need to remove data from the container registry,
  # which is not managed by the DB. Hence we're still using dependent: :destroy
  # here.
206
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
207

208
  has_many :commit_statuses
209
  has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
210 211 212 213 214

  # Ci::Build objects store data on the file system such as artifact files and
  # build traces. Currently there's no efficient way of removing this data in
  # bulk that doesn't involve loading the rows into memory. As a result we're
  # still using `dependent: :destroy` here.
215
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
216
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
217
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
218
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
219
  has_many :variables, class_name: 'Ci::Variable'
220 221 222 223
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
224

Kamil Trzcinski's avatar
Kamil Trzcinski committed
225 226
  has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'

227
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
228
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
229

230 231
  has_many :project_badges, class_name: 'ProjectBadge'

232
  accepts_nested_attributes_for :variables, allow_destroy: true
233
  accepts_nested_attributes_for :project_feature, update_only: true
234
  accepts_nested_attributes_for :import_data
235
  accepts_nested_attributes_for :auto_devops, update_only: true
236

237
  delegate :name, to: :owner, allow_nil: true, prefix: true
238
  delegate :members, to: :team, prefix: true
239
  delegate :add_user, :add_users, to: :team
240
  delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
241

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
242
  # Validations
243
  validates :creator, presence: true, on: :create
244
  validates :description, length: { maximum: 2000 }, allow_blank: true
245
  validates :ci_config_path,
246
    format: { without: %r{(\.{2}|\A/)},
247
              message: 'cannot include leading slash or directory traversal.' },
248 249
    length: { maximum: 255 },
    allow_blank: true
250 251
  validates :name,
    presence: true,
252
    length: { maximum: 255 },
253
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
254
              message: Gitlab::Regex.project_name_regex_message }
255 256
  validates :path,
    presence: true,
257
    project_path: true,
258
    length: { maximum: 255 }
259

260
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
261
  validates :name, uniqueness: { scope: :namespace_id }
262
  validates :import_url, addressable_url: true, if: :external_import?
263
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
264
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
265
  validate :check_limit, on: :create
266
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
267
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
268
  validate :visibility_level_allowed_as_fork
269
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
270
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
271 272 273
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
274
  validates :variables, variable_duplicates: { scope: :environment_scope }
275

276
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
277

278
  # Scopes
279
  scope :pending_delete, -> { where(pending_delete: true) }
280
  scope :without_deleted, -> { where(pending_delete: false) }
281

282 283 284
  scope :with_storage_feature, ->(feature) { where('storage_version >= :version', version: HASHED_STORAGE_FEATURES[feature]) }
  scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) }
  scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) }
285

286 287
  # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
  scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
288 289
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

290
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
291
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
292
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
293
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
294
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
295
  scope :archived, -> { where(archived: true) }
296
  scope :non_archived, -> { where(archived: false) }
297
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
298
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
299
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
300
  scope :with_statistics, -> { includes(:statistics) }
301
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
302 303 304
  scope :inside_path, ->(path) do
    # We need routes alias rs for JOIN so it does not conflict with
    # includes(:route) which we use in ProjectsFinder.
305 306
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
307
  end
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
    access_level_attribute = ProjectFeature.access_level_attribute(feature)
    with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
  }

  # Picks a feature where the level is exactly that given.
  scope :with_feature_access_level, ->(feature, level) {
    access_level_attribute = ProjectFeature.access_level_attribute(feature)
    with_project_feature.where(project_features: { access_level_attribute => level })
  }

  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
323
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
324
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
325

326
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
327

328 329
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
330 331
  def self.public_or_visible_to_user(user = nil)
    if user
332 333 334
      where('EXISTS (?) OR projects.visibility_level IN (?)',
            user.authorizations_for_projects,
            Gitlab::VisibilityLevel.levels_for_user(user))
335
    else
336
      public_to_user
337 338 339
    end
  end

340 341 342
  # project features may be "disabled", "internal" or "enabled". If "internal",
  # they are only available to team members. This scope returns projects where
  # the feature is either enabled, or internal with permission for the user.
343 344 345 346
  #
  # This method uses an optimised version of `with_feature_access_level` for
  # logged in users to more efficiently get private projects with the given
  # feature.
347
  def self.with_feature_available_for_user(feature, user)
348 349 350 351 352 353 354
    visible = [nil, ProjectFeature::ENABLED]

    if user&.admin?
      with_feature_enabled(feature)
    elsif user
      column = ProjectFeature.quoted_access_level_column(feature)

355 356
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
357 358
              visible,
              ProjectFeature::PRIVATE,
359
              user.authorizations_for_projects)
360 361 362
    else
      with_feature_access_level(feature, visible)
    end
363
  end
364

365 366
  scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
  scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
367

368
  scope :excluding_project, ->(project) { where.not(id: project) }
369
  scope :import_started, -> { where(import_status: 'started') }
370

371
  state_machine :import_status, initial: :none do
372 373 374 375 376 377 378 379
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

    event :force_import_start do
      transition [:none, :finished, :failed] => :started
    end

380
    event :import_start do
381
      transition scheduled: :started
382 383 384
    end

    event :import_finish do
385
      transition started: :finished
386 387 388
    end

    event :import_fail do
389
      transition [:scheduled, :started] => :failed
390 391 392
    end

    event :import_retry do
393
      transition failed: :started
394 395
    end

396
    state :scheduled
397 398
    state :started
    state :finished
399 400
    state :failed

401
    after_transition [:none, :finished, :failed] => :scheduled do |project, _|
402 403 404 405
      project.run_after_commit do
        job_id = add_import_job
        update(import_jid: job_id) if job_id
      end
406 407
    end

408 409
    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs
410 411 412

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        project.run_after_commit do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
413
          Projects::AfterImportService.new(project).execute
414 415
        end
      end
416
    end
417 418
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
419
  class << self
420 421 422 423 424 425 426
    # Searches for a list of projects based on the query given in `query`.
    #
    # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
    # search. On MySQL a regular "LIKE" is used as it's already
    # case-insensitive.
    #
    # query - The search query as a String.
427
    def search(query)
428
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
429
    end
430

431
    def search_by_title(query)
432
      non_archived.fuzzy_search(query, [:name])
433 434
    end

435 436 437
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
438 439

    def sort(method)
440 441
      case method.to_s
      when 'storage_size_desc'
442 443 444
        # storage_size is a joined column so we need to
        # pass a string to avoid AR adding the table name
        reorder('project_statistics.storage_size DESC, projects.id DESC')
445 446 447 448
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
449 450
      else
        order_by(method)
451 452
      end
    end
453 454

    def reference_pattern
455
      %r{
456 457
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
458
      }x
459
    end
460

461
    def trending
462 463
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
464
    end
465 466 467 468 469 470

    def cached_count
      Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
        Project.count
      end
    end
471 472

    def group_ids
473
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
474
    end
475 476
  end

477 478 479 480 481 482 483
  # returns all ancestor-groups upto but excluding the given namespace
  # when no namespace is given, all ancestors upto the top are returned
  def ancestors_upto(top = nil)
    Gitlab::GroupHierarchy.new(Group.where(id: namespace_id))
      .base_and_ancestors(upto: top)
  end

484
  def lfs_enabled?
485
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
486

487
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
488 489
  end

490
  def auto_devops_enabled?
491
    if auto_devops&.enabled.nil?
492
      Gitlab::CurrentSettings.auto_devops_enabled?
493 494
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
495
    end
496 497
  end

498
  def has_auto_devops_implicitly_disabled?
499
    auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
500 501
  end

502 503 504 505
  def empty_repo?
    repository.empty?
  end

506
  def repository_storage_path
507
    Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
508 509
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
510
  def team
511
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
512 513 514
  end

  def repository
515
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
516 517
  end

518 519
  def cleanup
    @repository&.cleanup
520 521 522
    @repository = nil
  end

523 524
  alias_method :reload_repository!, :cleanup

525
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
526
    if Gitlab.config.registry.enabled
527
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
528
    end
529 530
  end

531
  def has_container_registry_tags?
532 533 534
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
535
      has_root_container_repository_tags?
536 537
  end

538 539
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
540 541
  end

542 543 544 545
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

546
  # ref can't be HEAD, can only be branch/tag name or SHA
547
  def latest_successful_builds_for(ref = default_branch)
548
    latest_pipeline = pipelines.latest_successful_for(ref)
549 550

    if latest_pipeline
551
      latest_pipeline.builds.latest.with_artifacts_archive
552 553 554
    else
      builds.none
    end
555 556
  end

557
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
558
    sha = repository.merge_base(first_commit_id, second_commit_id)
559
    commit_by(oid: sha) if sha
560 561
  end

562
  def saved?
563
    id && persisted?
564 565
  end

566
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
567 568
    job_id =
      if forked?
569 570
        RepositoryForkWorker.perform_async(id,
                                           forked_from_project.repository_storage_path,
571
                                           forked_from_project.disk_path)
572
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
573
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
574
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
575 576 577
      else
        RepositoryImportWorker.perform_async(self.id)
      end
578

579 580 581 582 583 584 585 586
    log_import_activity(job_id)

    job_id
  end

  def log_import_activity(job_id, type: :import)
    job_type = type.to_s.capitalize

587
    if job_id
588
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
589
    else
590
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
591
    end
592 593
  end

594 595 596 597 598
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

599
    update(import_error: nil)
600 601 602 603 604
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
605
    import_data&.destroy
606 607
  end

608
  def ci_config_path=(value)
609
    # Strip all leading slashes so that //foo -> foo
610
    super(value&.delete("\0"))
611 612
  end

613
  def import_url=(value)
614 615
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

616
    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
617
    super(import_url.sanitized_url)
618
    create_or_update_import_data(credentials: import_url.credentials)
619 620 621
  end

  def import_url
James Lopez's avatar
James Lopez committed
622
    if import_data && super.present?
623
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
624 625 626
      import_url.full_url
    else
      super
627 628
    end
  end
629

James Lopez's avatar
James Lopez committed
630
  def valid_import_url?
631
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
632 633
  end

634
  def create_or_update_import_data(data: nil, credentials: nil)
635
    return unless import_url.present? && valid_import_url?
636

James Lopez's avatar
James Lopez committed
637
    project_import_data = import_data || build_import_data
638 639 640 641
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
642

643 644 645 646
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
647
  end
648

649
  def import?
650
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
651 652
  end

653 654 655 656
  def no_import?
    import_status == 'none'
  end

657
  def external_import?
658 659 660
    import_url.present?
  end

661
  def imported?
662 663 664 665
    import_finished?
  end

  def import_in_progress?
666 667 668 669
    import_started? || import_scheduled?
  end

  def import_started?
670 671
    # import? does SQL work so only run it if it looks like there's an import running
    import_status == 'started' && import?
672 673
  end

674 675 676 677
  def import_scheduled?
    import_status == 'scheduled'
  end

678 679 680 681 682 683
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
684 685
  end

686
  def safe_import_url
687
    Gitlab::UrlSanitizer.new(import_url).masked_url
688 689
  end

690 691 692 693
  def bare_repository_import?
    import_type == 'bare_repository'
  end

694 695 696 697
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
698 699 700 701
  def gitea_import?
    import_type == 'gitea'
  end

702
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
703
    unless creator.can_create_project? || namespace.kind == 'group'
704 705 706
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
707
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
708
      else
Phil Hughes's avatar
Phil Hughes committed
709
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
710
      end
711 712
    end
  rescue
713
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
714 715
  end

716 717 718 719 720 721 722 723 724 725 726 727 728
  def visibility_level_allowed_by_group
    return if visibility_level_allowed_by_group?

    level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
    group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
    self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
  end

  def visibility_level_allowed_as_fork
    return if visibility_level_allowed_as_fork?

    level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
    self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
729 730
  end

731 732 733 734 735 736 737 738 739 740
  def check_wiki_path_conflict
    return if path.blank?

    path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki"

    if Project.where(namespace_id: namespace_id, path: path_to_check).exists?
      errors.add(:name, 'has already been taken')
    end
  end

Rob Watson's avatar
Rob Watson committed
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
  def pages_https_only
    return false unless Gitlab.config.pages.external_https

    super
  end

  def pages_https_only?
    return false unless Gitlab.config.pages.external_https

    super
  end

  def validate_pages_https_only
    return unless pages_https_only?

    unless pages_domains.all?(&:https?)
      errors.add(:pages_https_only, "cannot be enabled unless all domains have TLS certificates")
    end
  end

761
  def to_param
762 763 764 765 766
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
767 768
  end

769
  # `from` argument can be a Namespace or Project.
770 771
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
772
      full_path
773 774 775
    elsif cross_project_reference?(from)
      path
    end
776 777
  end

778 779
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
780
      name_with_namespace
781
    elsif cross_project_reference?(from)
782 783
      name
    end
784 785
  end

786
  def web_url
787
    Gitlab::Routing.url_helpers.project_url(self)
788 789
  end

790
  def new_issuable_address(author, address_type)
791
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
792

793 794
    author.ensure_incoming_email_token!

795
    suffix = address_type == 'merge_request' ? '+merge-request' : ''
796
    Gitlab::IncomingEmail.reply_address(
797
      "#{full_path}#{suffix}+#{author.incoming_email_token}")
798 799
  end

800
  def build_commit_note(commit)
801
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
802
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
803

804
  def last_activity
805
    last_event
gitlabhq's avatar
gitlabhq committed
806 807 808
  end

  def last_activity_date
809
    [last_activity_at, last_repository_updated_at, updated_at].compact.max
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
810
  end
811

812 813 814
  def project_id
    self.id
  end
randx's avatar
randx committed
815

816
  def get_issue(issue_id, current_user)
817 818 819 820 821
    issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?

    if issue
      issue
    elsif external_issue_tracker
Robert Speicher's avatar
Robert Speicher committed
822
      ExternalIssue.new(issue_id, self)
823 824 825
    end
  end

Robert Speicher's avatar
Robert Speicher committed
826
  def issue_exists?(issue_id)
827
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
828 829
  end

830
  def default_issue_tracker
831
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
832 833 834 835 836 837 838 839 840 841
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

842
  def external_issue_reference_pattern
843
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
844 845
  end

846
  def default_issues_tracker?
847
    !external_issue_tracker
848 849 850
  end

  def external_issue_tracker
851 852 853 854 855 856 857 858 859 860 861 862 863 864
    if has_external_issue_tracker.nil? # To populate existing projects
      cache_has_external_issue_tracker
    end

    if has_external_issue_tracker?
      return @external_issue_tracker if defined?(@external_issue_tracker)

      @external_issue_tracker = services.external_issue_trackers.first
    else
      nil
    end
  end

  def cache_has_external_issue_tracker
865
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
866 867
  end

868 869 870 871
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

872 873 874 875 876 877 878 879 880 881 882 883 884
  def external_wiki
    if has_external_wiki.nil?
      cache_has_external_wiki # Populate
    end

    if has_external_wiki
      @external_wiki ||= services.external_wikis.first
    else
      nil
    end
  end

  def cache_has_external_wiki
885
    update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
886 887
  end

888
  def find_or_initialize_services(exceptions: [])
889 890
    services_templates = Service.where(template: true)

891 892 893
    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
894
      service = find_service(services, service_name)
895

896 897 898
      if service
        service
      else
899 900 901 902
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
903
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
904
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
905
        else
906
          Service.build_from_template(id, template)
907 908
        end
      end
909 910 911
    end
  end

912 913 914 915
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

916 917
  def create_labels
    Label.templates.each do |label|
918
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
919
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
920 921 922
    end
  end

923 924 925
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
926

927
  def ci_services
928
    services.where(category: :ci)
929 930 931
  end

  def ci_service
932
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
933 934
  end

935 936 937 938 939
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
940
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
941 942
  end

Drew Blessing's avatar
Drew Blessing committed
943 944 945 946
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

947
  def avatar_in_git
948
    repository.avatar
949 950
  end

951
  def avatar_url(**args)
952
    Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
sue445's avatar
sue445 committed
953 954
  end

955 956 957 958 959
  # For compatibility with old code
  def code
    path
  end

960
  def items_for(entity)
961 962 963 964 965 966 967
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
968

969
  def send_move_instructions(old_path_with_namespace)
970 971
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
972 973 974
    run_after_commit do
      NotificationService.new.project_was_moved(self, old_path_with_namespace)
    end
975
  end
976 977

  def owner
978 979
    if group
      group
980
    else
981
      namespace.try(:owner)
982 983
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
984

985
  def execute_hooks(data, hooks_scope = :push_hooks)
986
    run_after_commit_or_now do
987
      hooks.hooks_for(hooks_scope).each do |hook|
988 989
        hook.async_execute(data, hooks_scope.to_s)
      end
990

991 992
      SystemHooksService.new.execute_hooks(data, hooks_scope)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
993 994
  end

995 996
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
997 998 999 1000
    run_after_commit_or_now do
      services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
        service.async_execute(data)
      end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1001 1002 1003 1004
    end
  end

  def valid_repo?
1005
    repository.exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1006
  rescue
1007
    errors.add(:path, 'Invalid repository path')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1008 1009 1010 1011
    false
  end

  def url_to_repo
1012
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1013 1014 1015
  end

  def repo_exists?
1016 1017 1018 1019 1020 1021 1022
    strong_memoize(:repo_exists) do
      begin
        repository.exists?
      rescue
        false
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1023 1024 1025
  end

  def root_ref?(branch)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1026
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1027 1028 1029 1030 1031 1032
  end

  def ssh_url_to_repo
    url_to_repo
  end

1033 1034
  def http_url_to_repo
    "#{web_url}.git"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1035 1036
  end

1037
  def user_can_push_to_empty_repo?(user)
1038 1039 1040
    return false unless empty_repo?
    return false unless Ability.allowed?(user, :push_code, self)

1041
    !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
1042 1043
  end

1044
  def forked?
1045 1046 1047 1048 1049
    return true if fork_network && fork_network.root_project != self

    # TODO: Use only the above conditional using the `fork_network`
    # This is the old conditional that looks at the `forked_project_link`, we
    # fall back to this while we're migrating the new models
1050 1051
    !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1052

1053
  def fork_source
1054 1055
    return nil unless forked?

1056 1057 1058
    forked_from_project || fork_network&.root_project
  end

1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
  def lfs_storage_project
    @lfs_storage_project ||= begin
      result = self

      # TODO: Make this go to the fork_network root immeadiatly
      # dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
      result = result.fork_source while result&.forked?

      result || self
    end
  end

1071 1072 1073 1074
  def personal?
    !group
  end

1075 1076 1077 1078 1079 1080
  # Expires various caches before a project is renamed.
  def expire_caches_before_rename(old_path)
    repo = Repository.new(old_path, self)
    wiki = Repository.new("#{old_path}.wiki", self)

    if repo.exists?
1081
      repo.before_delete
1082 1083 1084
    end

    if wiki.exists?
1085
      wiki.before_delete
1086 1087 1088
    end
  end

1089
  # Check if repository already exists on disk
1090 1091
  def check_repository_path_availability
    return true if skip_disk_validation
1092 1093 1094 1095
    return false unless repository_storage_path

    expires_full_path_cache # we need to clear cache to validate renames correctly

1096 1097 1098
    # Check if repository with same path already exists on disk we can
    # skip this for the hashed storage because the path does not change
    if legacy_storage? && repository_with_same_path_already_exists?
1099 1100 1101 1102 1103
      errors.add(:base, 'There is already a repository with that name on disk')
      return false
    end

    true
1104 1105
  rescue GRPC::Internal # if the path is too long
    false
1106 1107
  end

1108 1109 1110 1111
  def create_repository(force: false)
    # Forked import is handled asynchronously
    return if forked? && !force

1112
    if gitlab_shell.create_repository(repository_storage, disk_path)
1113 1114 1115 1116 1117 1118 1119 1120
      repository.after_create
      true
    else
      errors.add(:base, 'Failed to create repository via gitlab-shell')
      false
    end
  end

1121 1122
  def hook_attrs(backward: true)
    attrs = {
1123
      id: id,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1124
      name: name,
1125
      description: description,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
1126
      web_url: web_url,
1127
      avatar_url: avatar_url(only_path: false),
1128 1129
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1130
      namespace: namespace.name,
1131
      visibility_level: visibility_level,
1132
      path_with_namespace: full_path,
1133
      default_branch: default_branch,
1134
      ci_config_path: ci_config_path
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1135
    }
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147

    # Backward compatibility
    if backward
      attrs.merge!({
                    homepage: web_url,
                    url: url_to_repo,
                    ssh_url: ssh_url_to_repo,
                    http_url: http_url_to_repo
                  })
    end

    attrs
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1148 1149
  end

1150
  def project_member(user)
1151 1152 1153 1154 1155
    if project_members.loaded?
      project_members.find { |member| member.user_id == user.id }
    else
      project_members.find_by(user_id: user)
    end
1156
  end
1157 1158 1159 1160

  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1161 1162 1163 1164 1165

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1166

1167
  def visibility_level_field
1168
    :visibility_level
1169
  end
1170 1171 1172 1173 1174 1175 1176 1177

  def archive!
    update_attribute(:archived, true)
  end

  def unarchive!
    update_attribute(:archived, false)
  end
1178

1179
  def change_head(branch)
1180 1181
    if repository.branch_exists?(branch)
      repository.before_change_head
1182
      repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false)
1183 1184 1185 1186 1187 1188 1189
      repository.copy_gitattributes(branch)
      repository.after_change_head
      reload_default_branch
    else
      errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist")
      false
    end
1190
  end
1191

1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204
  def forked_from?(other_project)
    forked? && forked_from_project == other_project
  end

  def in_fork_network_of?(other_project)
    # TODO: Remove this in a next release when all fork_networks are populated
    # This makes sure all MergeRequests remain valid while the projects don't
    # have a fork_network yet.
    return true if forked_from?(other_project)

    return false if fork_network.nil? || other_project.fork_network.nil?

    fork_network == other_project.fork_network
1205
  end
1206

1207 1208 1209
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1210

1211
  def ensure_repository
1212
    create_repository(force: true) unless repository_exists?
1213 1214
  end

1215 1216 1217 1218
  def repository_exists?
    !!repository.exists?
  end

1219 1220 1221 1222
  def wiki_repository_exists?
    wiki.repository_exists?
  end

1223
  # update visibility_level of forks
1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234
  def update_forks_visibility_level
    return unless visibility_level < visibility_level_was

    forks.each do |forked_project|
      if forked_project.visibility_level > visibility_level
        forked_project.visibility_level = visibility_level
        forked_project.save!
      end
    end
  end

1235 1236 1237
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
1238
  rescue ProjectWiki::CouldNotCreateWikiError
1239
    errors.add(:base, 'Failed create wiki')
1240 1241
    false
  end
1242

1243 1244 1245 1246
  def wiki
    @wiki ||= ProjectWiki.new(self, self.owner)
  end

Drew Blessing's avatar
Drew Blessing committed
1247 1248 1249 1250
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end

1251
  def allowed_to_share_with_group?
1252
    !namespace.share_with_group_lock
1253 1254
  end

1255 1256 1257
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1258
    return unless sha
1259

1260
    pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
1261 1262
  end

1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279
  def latest_successful_pipeline_for_default_branch
    if defined?(@latest_successful_pipeline_for_default_branch)
      return @latest_successful_pipeline_for_default_branch
    end

    @latest_successful_pipeline_for_default_branch =
      pipelines.latest_successful_for(default_branch)
  end

  def latest_successful_pipeline_for(ref = nil)
    if ref && ref != default_branch
      pipelines.latest_successful_for(ref)
    else
      latest_successful_pipeline_for_default_branch
    end
  end

1280
  def enable_ci
1281
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1282
  end
Marin Jankovski's avatar
Marin Jankovski committed
1283

1284 1285 1286 1287 1288
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1289
    @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
1290 1291
  end

1292 1293
  def active_shared_runners
    @active_shared_runners ||= shared_runners.active
1294
  end
1295 1296

  def any_runners?(&block)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1297
    active_runners.any?(&block) || active_shared_runners.any?(&block)
1298 1299
  end

1300
  def valid_runners_token?(token)
James Lopez's avatar
James Lopez committed
1301
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1302 1303
  end

1304 1305 1306 1307 1308 1309 1310
  def build_timeout_in_minutes
    build_timeout / 60
  end

  def build_timeout_in_minutes=(value)
    self.build_timeout = value.to_i * 60
  end
1311

1312
  def open_issues_count
1313 1314 1315 1316 1317
    Projects::OpenIssuesCountService.new(self).count
  end

  def open_merge_requests_count
    Projects::OpenMergeRequestsCountService.new(self).count
1318
  end
1319

1320
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
Douwe Maan's avatar
Douwe Maan committed
1321
    return true unless forked?
1322

Douwe Maan's avatar
Douwe Maan committed
1323 1324
    # self.forked_from_project will be nil before the project is saved, so
    # we need to go through the relation
1325
    original_project = forked_project_link&.forked_from_project
Douwe Maan's avatar
Douwe Maan committed
1326 1327 1328
    return true unless original_project

    level <= original_project.visibility_level
1329
  end
1330

1331 1332
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1333

1334
    level <= group.visibility_level
Marin Jankovski's avatar
Marin Jankovski committed
1335
  end
1336

1337 1338
  def visibility_level_allowed?(level = self.visibility_level)
    visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
Felipe Artur's avatar
Felipe Artur committed
1339 1340
  end

1341 1342 1343
  def runners_token
    ensure_runners_token!
  end
1344

1345 1346 1347
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1348

1349
  def pages_group_url
1350
    # The host in URL always needs to be downcased
1351 1352
    Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
      "#{prefix}#{pages_subdomain}."
1353
    end.downcase
1354 1355 1356 1357 1358
  end

  def pages_url
    url = pages_group_url
    url_path = full_path.partition('/').last
1359

1360
    # If the project path is the same as host, we serve it as group page
1361
    return url if url == "#{Settings.pages.protocol}://#{url_path}"
1362 1363 1364

    "#{url}/#{url_path}"
  end
1365

1366 1367
  def pages_subdomain
    full_path.partition('/').first
1368
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1369 1370

  def pages_path
1371 1372
    # TODO: when we migrate Pages to work with new storage types, change here to use disk_path
    File.join(Settings.pages.path, full_path)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1373 1374 1375 1376 1377 1378
  end

  def public_pages_path
    File.join(pages_path, 'public')
  end

1379 1380 1381 1382
  def pages_available?
    Gitlab.config.pages.enabled && !namespace.subgroup?
  end

1383
  def remove_private_deploy_keys
1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
    exclude_keys_linked_to_other_projects = <<-SQL
      NOT EXISTS (
        SELECT 1
        FROM deploy_keys_projects dkp2
        WHERE dkp2.deploy_key_id = deploy_keys_projects.deploy_key_id
        AND dkp2.project_id != deploy_keys_projects.project_id
      )
    SQL

    deploy_keys.where(public: false)
               .where(exclude_keys_linked_to_other_projects)
               .delete_all
1396 1397
  end

1398
  # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1399
  def remove_pages
1400 1401 1402
    # Projects with a missing namespace cannot have their pages removed
    return unless namespace

1403 1404
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1405 1406 1407
    # 1. We rename pages to temporary directory
    # 2. We wait 5 minutes, due to NFS caching
    # 3. We asynchronously remove pages with force
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1408
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
1409

1410 1411
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
1412
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1413 1414
  end

1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441
  def rename_repo
    new_full_path = build_full_path

    Rails.logger.error "Attempting to rename #{full_path_was} -> #{new_full_path}"

    if has_container_registry_tags?
      Rails.logger.error "Project #{full_path_was} cannot be renamed because container registry tags are present!"

      # we currently doesn't support renaming repository if it contains images in container registry
      raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
    end

    expire_caches_before_rename(full_path_was)

    if storage.rename_repo
      Gitlab::AppLogger.info "Project was renamed: #{full_path_was} -> #{new_full_path}"
      rename_repo_notify!
      after_rename_repo
    else
      Rails.logger.error "Repository could not be renamed: #{full_path_was} -> #{new_full_path}"

      # if we cannot move namespace directory we should rollback
      # db changes in order to prevent out of sync between db and fs
      raise StandardError.new('repository cannot be renamed')
    end
  end

1442
  def after_rename_repo
1443
    write_repository_config
1444

1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456
    path_before_change = previous_changes['path'].first

    # We need to check if project had been rolled out to move resource to hashed storage or not and decide
    # if we need execute any take action or no-op.

    unless hashed_storage?(:attachments)
      Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
    end

    Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
  end

1457 1458 1459 1460
  def write_repository_config(gl_full_path: full_path)
    # We'd need to keep track of project full path otherwise directory tree
    # created with hashed storage enabled cannot be usefully imported using
    # the import rake task.
1461
    repository.raw_repository.write_config(full_path: gl_full_path)
1462
  rescue Gitlab::Git::Repository::NoRepository => e
1463
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
1464
    nil
1465 1466
  end

1467 1468 1469 1470 1471 1472 1473 1474 1475 1476
  def rename_repo_notify!
    send_move_instructions(full_path_was)
    expires_full_path_cache

    self.old_path_with_namespace = full_path_was
    SystemHooksService.new.execute_hooks_for(self, :rename)

    reload_repository!
  end

1477 1478 1479 1480 1481
  def after_import
    repository.after_import
    import_finish
    remove_import_jid
    update_project_counter_caches
1482
    after_create_default_branch
1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495
  end

  def update_project_counter_caches
    classes = [
      Projects::OpenIssuesCountService,
      Projects::OpenMergeRequestsCountService
    ]

    classes.each do |klass|
      klass.new(self).refresh_cache
    end
  end

1496 1497 1498 1499 1500 1501
  def after_create_default_branch
    return unless default_branch

    # Ensure HEAD points to the default branch in case it is not master
    change_head(default_branch)

1502
    if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
1503 1504 1505
      params = {
        name: default_branch,
        push_access_levels_attributes: [{
1506
          access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
1507 1508
        }],
        merge_access_levels_attributes: [{
1509
          access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
1510 1511 1512 1513 1514 1515 1516
        }]
      }

      ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true)
    end
  end

1517 1518 1519 1520 1521 1522 1523
  def remove_import_jid
    return unless import_jid

    Gitlab::SidekiqStatus.unset(import_jid)
    update_column(:import_jid, nil)
  end

Josh Frye's avatar
Josh Frye committed
1524 1525
  def running_or_pending_build_count(force: false)
    Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
1526 1527 1528
      builds.running_or_pending.count(:all)
    end
  end
1529

1530
  # Lazy loading of the `pipeline_status` attribute
1531
  def pipeline_status
1532
    @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
1533 1534
  end

1535
  def mark_import_as_failed(error_message)
1536 1537 1538
    original_errors = errors.dup
    sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)

1539
    import_fail
1540 1541 1542 1543 1544
    update_column(:import_error, sanitized_message)
  rescue ActiveRecord::ActiveRecordError => e
    Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
  ensure
    @errors = original_errors
1545
  end
James Lopez's avatar
James Lopez committed
1546

1547 1548
  def add_export_job(current_user:, after_export_strategy: nil, params: {})
    job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
1549 1550 1551 1552 1553 1554 1555

    if job_id
      Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
    else
      Rails.logger.error "Export job failed to start for project ID #{self.id}"
    end
  end
James Lopez's avatar
James Lopez committed
1556

1557 1558
  def import_export_shared
    @import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
1559 1560
  end

James Lopez's avatar
James Lopez committed
1561
  def export_path
1562 1563
    return nil unless namespace.present? || hashed_storage?(:repository)

1564
    import_export_shared.archive_path
James Lopez's avatar
James Lopez committed
1565
  end
1566 1567 1568 1569 1570

  def export_project_path
    Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
  end

1571 1572 1573
  def export_status
    if export_in_progress?
      :started
1574 1575
    elsif after_export_in_progress?
      :after_export_action
1576 1577 1578 1579 1580 1581 1582 1583
    elsif export_project_path
      :finished
    else
      :none
    end
  end

  def export_in_progress?
1584
    import_export_shared.active_export_count > 0
1585 1586
  end

1587 1588 1589 1590
  def after_export_in_progress?
    import_export_shared.after_export_in_progress?
  end

1591
  def remove_exports
1592 1593 1594
    return nil unless export_path.present?

    FileUtils.rm_rf(export_path)
1595
  end
1596

1597 1598 1599 1600 1601 1602
  def remove_exported_project_file
    return unless export_project_path.present?

    FileUtils.rm_f(export_project_path)
  end

1603 1604 1605 1606
  def full_path_slug
    Gitlab::Utils.slugify(full_path.to_s)
  end

1607
  def has_ci?
1608
    repository.gitlab_ci_yml || auto_devops_enabled?
1609 1610
  end

1611
  def predefined_variables
1612 1613
    visibility = Gitlab::VisibilityLevel.string_level(visibility_level)

1614 1615 1616 1617 1618 1619 1620 1621 1622 1623
    Gitlab::Ci::Variables::Collection.new
      .append(key: 'CI_PROJECT_ID', value: id.to_s)
      .append(key: 'CI_PROJECT_NAME', value: path)
      .append(key: 'CI_PROJECT_PATH', value: full_path)
      .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
      .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
      .append(key: 'CI_PROJECT_URL', value: web_url)
      .append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
      .concat(container_registry_variables)
      .concat(auto_devops_variables)
1624 1625 1626
  end

  def container_registry_variables
1627 1628
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
      return variables unless Gitlab.config.registry.enabled
1629

1630
      variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
1631

1632
      if container_registry_enabled?
1633
        variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url)
1634
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1635
    end
1636 1637
  end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
1638 1639
  def secret_variables_for(ref:, environment: nil)
    # EE would use the environment
1640 1641 1642 1643 1644 1645
    if protected_for?(ref)
      variables
    else
      variables.unprotected
    end
  end
1646

1647
  def protected_for?(ref)
1648 1649 1650
    if repository.branch_exists?(ref)
      ProtectedBranch.protected?(self, ref)
    elsif repository.tag_exists?(ref)
1651
      ProtectedTag.protected?(self, ref)
1652
    end
1653
  end
1654

1655 1656
  def deployment_variables(environment: nil)
    deployment_platform(environment: environment)&.predefined_variables || []
1657 1658
  end

1659 1660 1661
  def auto_devops_variables
    return [] unless auto_devops_enabled?

1662
    (auto_devops || build_auto_devops)&.predefined_variables
1663 1664
  end

1665
  def append_or_update_attribute(name, value)
1666
    old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
1667 1668 1669 1670 1671 1672

    if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
      update_attribute(name, old_values + value)
    else
      update_attribute(name, value)
    end
1673 1674 1675

  rescue ActiveRecord::RecordNotSaved => e
    handle_update_attribute_error(e, value)
1676 1677
  end

1678
  def pushes_since_gc
1679
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
1680 1681 1682
  end

  def increment_pushes_since_gc
1683
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
1684 1685 1686
  end

  def reset_pushes_since_gc
1687
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
1688 1689
  end

Douwe Maan's avatar
Douwe Maan committed
1690
  def route_map_for(commit_sha)
1691 1692
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
Douwe Maan's avatar
Douwe Maan committed
1693
        data = repository.route_map_for(sha)
1694 1695
        next unless data

Douwe Maan's avatar
Douwe Maan committed
1696 1697 1698
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1699 1700 1701 1702 1703 1704 1705
      end
    end

    @route_maps_by_commit[commit_sha]
  end

  def public_path_for_source_path(path, commit_sha)
Douwe Maan's avatar
Douwe Maan committed
1706
    map = route_map_for(commit_sha)
1707 1708
    return unless map

Douwe Maan's avatar
Douwe Maan committed
1709
    map.public_path_for_source_path(path)
1710 1711
  end

1712 1713 1714 1715
  def parent_changed?
    namespace_id_changed?
  end

1716 1717 1718 1719 1720 1721 1722 1723
  def default_merge_request_target
    if forked_from_project&.merge_requests_enabled?
      forked_from_project
    else
      self
    end
  end

Felipe Artur's avatar
Felipe Artur committed
1724 1725 1726
  # Overridden on EE module
  def multiple_issue_boards_available?
    false
Felipe Artur's avatar
Felipe Artur committed
1727 1728 1729 1730 1731 1732
  end

  def issue_board_milestone_available?(user = nil)
    feature_available?(:issue_board_milestone, user)
  end

1733 1734 1735 1736
  def full_path_was
    File.join(namespace.full_path, previous_changes['path'].first)
  end

1737 1738
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
1739
  # @deprecated cannot remove yet because it has an index with its name in elasticsearch
1740 1741
  alias_method :path_with_namespace, :full_path

1742 1743 1744 1745
  def forks_count
    Projects::ForksCountService.new(self).count
  end

1746
  def legacy_storage?
1747 1748 1749
    [nil, 0].include?(self.storage_version)
  end

1750 1751 1752 1753
  # Check if Hashed Storage is enabled for the project with at least informed feature rolled out
  #
  # @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments)
  def hashed_storage?(feature)
1754 1755 1756
    raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)

    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
1757 1758
  end

1759 1760 1761 1762
  def renamed?
    persisted? && path_changed?
  end

1763 1764 1765
  def merge_method
    if self.merge_requests_ff_only_enabled
      :ff
1766 1767
    elsif self.merge_requests_rebase_enabled
      :rebase_merge
1768 1769 1770 1771 1772 1773
    else
      :merge
    end
  end

  def merge_method=(method)
1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784
    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
1785 1786 1787
  end

  def ff_merge_must_be_possible?
1788
    self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
1789 1790
  end

1791
  def migrate_to_hashed_storage!
1792
    return if hashed_storage?(:repository)
1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812

    update!(repository_read_only: true)

    if repo_reference_count > 0 || wiki_reference_count > 0
      ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
    else
      ProjectMigrateHashedStorageWorker.perform_async(id)
    end
  end

  def storage_version=(value)
    super

    @storage = nil if storage_version_changed?
  end

  def gl_repository(is_wiki:)
    Gitlab::GlRepository.gl_repository(self, is_wiki)
  end

1813 1814 1815 1816
  def reference_counter(wiki: false)
    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
  end

1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827
  # Refreshes the expiration time of the associated import job ID.
  #
  # This method can be used by asynchronous importers to refresh the status,
  # preventing the StuckImportJobsWorker from marking the import as failed.
  def refresh_import_jid_expiration
    return unless import_jid

    Gitlab::SidekiqStatus
      .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
  end

1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838
  def badges
    return project_badges unless group

    group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)

    union = Gitlab::SQL::Union.new([project_badges.select(:id),
                                    group_badges_rel.select(:id)])

    Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
  end

1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860
  def merge_requests_allowing_push_to_user(user)
    return MergeRequest.none unless user

    developer_access_exists = user.project_authorizations
                                .where('access_level >= ? ', Gitlab::Access::DEVELOPER)
                                .where('project_authorizations.project_id = merge_requests.target_project_id')
                                .limit(1)
                                .select(1)
    source_of_merge_requests.opened
      .where(allow_maintainer_to_push: true)
      .where('EXISTS (?)', developer_access_exists)
  end

  def branch_allows_maintainer_push?(user, branch_name)
    return false unless user

    cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"

    memoized_results = strong_memoize(:branch_allows_maintainer_push) do
      Hash.new do |result, cache_key|
        result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name)
      end
1861 1862
    end

1863
    memoized_results[cache_key]
1864 1865
  end

1866 1867
  private

1868 1869
  def storage
    @storage ||=
1870
      if hashed_storage?(:repository)
1871 1872 1873 1874 1875
        Storage::HashedProject.new(self)
      else
        Storage::LegacyProject.new(self)
      end
  end
1876

1877
  def use_hashed_storage
1878
    if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
1879
      self.storage_version = LATEST_STORAGE_VERSION
1880 1881 1882
    end
  end

1883
  def repo_reference_count
1884
    reference_counter.value
1885 1886 1887
  end

  def wiki_reference_count
1888
    reference_counter(wiki: true).value
1889 1890
  end

1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903
  def check_repository_absence!
    return if skip_disk_validation

    if repository_storage_path.blank? || repository_with_same_path_already_exists?
      errors.add(:base, 'There is already a repository with that name on disk')
      throw :abort
    end
  end

  def repository_with_same_path_already_exists?
    gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
  end

1904 1905 1906 1907 1908 1909 1910 1911 1912
  # set last_activity_at to the same as created_at
  def set_last_activity_at
    update_column(:last_activity_at, self.created_at)
  end

  def set_last_repository_updated_at
    update_column(:last_repository_updated_at, self.created_at)
  end

1913
  def cross_namespace_reference?(from)
1914 1915 1916 1917 1918
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
1919 1920 1921
    end
  end

1922
  # Check if a reference is being done cross-project
1923 1924 1925 1926
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
1927 1928
  end

1929
  def pushes_since_gc_redis_shared_state_key
1930 1931 1932
    "projects/#{id}/pushes_since_gc"
  end

1933 1934 1935 1936 1937 1938 1939
  # Similar to the normal callbacks that hook into the life cycle of an
  # Active Record object, you can also define callbacks that get triggered
  # when you add an object to an association collection. If any of these
  # callbacks throw an exception, the object will not be added to the
  # collection. Before you add a new board to the boards collection if you
  # already have 1, 2, or n it will fail, but it if you have 0 that is lower
  # than the number of permitted boards per project it won't fail.
1940
  def validate_board_limit(board)
1941
    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
1942
  end
1943

1944 1945 1946 1947
  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962

  def check_pending_delete
    return if valid_attribute?(:name) && valid_attribute?(:path)
    return unless pending_delete_twin

    %i[route route.path name path].each do |error|
      errors.delete(error)
    end

    errors.add(:base, "The project is still being deleted. Please try again later.")
  end

  def pending_delete_twin
    return false unless path

1963
    Project.pending_delete.find_by_full_path(full_path)
1964
  end
1965 1966 1967 1968 1969 1970 1971 1972 1973

  ##
  # This method is here because of support for legacy container repository
  # which has exactly the same path like project does, but which might not be
  # persisted in `container_repositories` table.
  #
  def has_root_container_repository_tags?
    return false unless Gitlab.config.registry.enabled

1974
    ContainerRepository.build_root_repository(self).has_tags?
1975
  end
1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987

  def handle_update_attribute_error(ex, value)
    if ex.message.start_with?('Failed to replace')
      if value.respond_to?(:each)
        invalid = value.detect(&:invalid?)

        raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
      end
    end

    raise ex
  end
1988

1989
  def fetch_branch_allows_maintainer_push?(user, branch_name)
1990 1991 1992 1993 1994 1995 1996 1997
    check_access = -> do
      merge_request = source_of_merge_requests.opened
                        .where(allow_maintainer_to_push: true)
                        .find_by(source_branch: branch_name)

      merge_request&.can_be_merged_by?(user)
    end

1998
    if RequestStore.active?
1999
      RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
2000
        check_access.call
2001 2002
      end
    else
2003
      check_access.call
2004 2005
    end
  end
gitlabhq's avatar
gitlabhq committed
2006
end