project.rb 67.4 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
5
class Project < ActiveRecord::Base
6
  include Gitlab::ConfigHelper
7
  include Gitlab::ShellAdapter
8
  include Gitlab::VisibilityLevel
9
  include AccessRequestable
10
  include Avatarable
11
  include CacheMarkdownField
12 13
  include Referable
  include Sortable
14
  include AfterCommitQueue
15
  include CaseSensitivity
16
  include TokenAuthenticatable
17
  include ValidAttribute
18
  include ProjectFeaturesCompatibility
19
  include SelectForProjectAuthorization
20
  include Presentable
21
  include Routable
22
  include GroupDescendant
23
  include Gitlab::SQL::Pattern
24
  include DeploymentPlatform
25
  include ::Gitlab::Utils::StrongMemoize
26
  include ChronicDurationAttribute
27
  include FastDestroyAll::Helpers
Jan Provaznik's avatar
Jan Provaznik committed
28
  include WithUploads
29
  include BatchDestroyDependentAssociations
30
  include FeatureGate
31
  include OptionallySearch
32
  include FromUnion
33
  include IgnorableColumn
34
  extend Gitlab::Cache::RequestCache
Robert Speicher's avatar
Robert Speicher committed
35

36
  extend Gitlab::ConfigHelper
37

38
  BoardLimitExceeded = Class.new(StandardError)
39

40
  STATISTICS_ATTRIBUTE = 'repositories_count'.freeze
41
  NUMBER_OF_PERMITTED_BOARDS = 1
Douwe Maan's avatar
Douwe Maan committed
42
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
43 44
  # Hashed Storage versions handle rolling out new storage to project and dependents models:
  # nil: legacy
45 46 47
  # 1: repository
  # 2: attachments
  LATEST_STORAGE_VERSION = 2
48 49 50 51
  HASHED_STORAGE_FEATURES = {
    repository: 1,
    attachments: 2
  }.freeze
Jared Szechy's avatar
Jared Szechy committed
52

53 54 55 56 57
  VALID_IMPORT_PORTS = [80, 443].freeze
  VALID_IMPORT_PROTOCOLS = %w(http https git).freeze

  VALID_MIRROR_PORTS = [22, 80, 443].freeze
  VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
58

59 60
  ignore_column :import_status, :import_jid, :import_error

61 62
  cache_markdown_field :description, pipeline: :description

63
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
64 65
           :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
           to: :project_feature, allow_nil: true
66

67
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
68

69 70 71 72 73 74
  delegate :scheduled?, :started?, :in_progress?,
    :failed?, :finished?,
    prefix: :import, to: :import_state, allow_nil: true

  delegate :no_import?, to: :import_state, allow_nil: true

75
  default_value_for :archived, false
76
  default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility }
77
  default_value_for :resolve_outdated_diff_discussions, false
78
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
79 80
  default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
81 82 83 84 85
  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
86
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
87

88
  add_authentication_token_field :runners_token, encrypted: true, migrating: true
89

90
  before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
91

92
  before_save :ensure_runners_token
93

94
  after_save :update_project_statistics, if: :namespace_id_changed?
95 96 97

  after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }

98
  after_create :create_project_feature, unless: :project_feature
99 100 101 102 103

  after_create :create_ci_cd_settings,
    unless: :ci_cd_settings,
    if: proc { ProjectCiCdSetting.available? }

104
  after_create :set_timestamps_for_create
105
  after_update :update_forks_visibility_level
106

107
  before_destroy :remove_private_deploy_keys
108

109
  use_fast_destroy :build_trace_chunks
110

111
  after_destroy -> { run_after_commit { remove_pages } }
112
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
113

114 115
  after_validation :check_pending_delete

116
  # Storage specific hooks
117
  after_initialize :use_hashed_storage
118
  after_create :check_repository_absence!
119 120
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
121

122
  acts_as_ordered_taggable
123

124
  attr_accessor :old_path_with_namespace
125
  attr_accessor :template_name
126
  attr_writer :pipeline_status
127
  attr_accessor :skip_disk_validation
128

129 130
  alias_attribute :title, :name

131
  # Relations
132
  belongs_to :pool_repository
133
  belongs_to :creator, class_name: 'User'
134
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
135
  belongs_to :namespace
136 137
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
138

139
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
140
  has_many :boards, before_add: :validate_board_limit
141

142
  # Project services
143
  has_one :campfire_service
blackst0ne's avatar
blackst0ne committed
144
  has_one :discord_service
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  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 :flowdock_service
  has_one :assembla_service
  has_one :asana_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
173
  has_one :packagist_service
174
  has_one :hangouts_chat_service
175

176 177 178 179 180
  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
181
  has_one :fork_network, through: :fork_network_member
182 183 184
  has_one :forked_from_project, through: :fork_network_member
  has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
  has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
185

186
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
187
  has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
188
  has_one :project_repository, inverse_of: :project
189
  has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
190

191
  # Merge Requests for target project should be removed with it
192
  has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
193
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
194 195 196 197 198 199 200 201 202 203
  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
204
  has_many :repository_languages, -> { order "share DESC" }
205

206
  has_many :project_authorizations
207
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
208
  has_many :project_members, -> { where(requested_at: nil) },
209
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
210

211
  alias_method :members, :project_members
212
  has_many :users, through: :project_members
213

214
  has_many :requesters, -> { where.not(requested_at: nil) },
215
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
216
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
217

218
  has_many :deploy_keys_projects
219
  has_many :deploy_keys, through: :deploy_keys_projects
220
  has_many :users_star_projects
221
  has_many :starrers, through: :users_star_projects, source: :user
222
  has_many :releases
223
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
224
  has_many :lfs_objects, through: :lfs_objects_projects
225
  has_many :lfs_file_locks
226
  has_many :project_group_links
227
  has_many :invited_groups, through: :project_group_links, source: :group
228 229
  has_many :pages_domains
  has_many :todos
230
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
231

232 233
  has_many :internal_ids

234
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
235
  has_one :project_feature, inverse_of: :project
236
  has_one :statistics, class_name: 'ProjectStatistics'
237

Shinya Maeda's avatar
Shinya Maeda committed
238
  has_one :cluster_project, class_name: 'Clusters::Project'
239
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
240
  has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
241
  has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
242

243 244
  has_many :prometheus_metrics

245 246 247
  # 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.
248
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
249

250
  has_many :commit_statuses
251
  # The relation :all_pipelines is intented to be used when we want to get the
252 253
  # whole list of pipelines associated to the project
  has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
254
  # The relation :ci_pipelines is intented to be used when we want to get only
255 256 257 258
  # those pipeline which are directly related to CI. There are
  # other pipelines, like webide ones, that we won't retrieve
  # if we use this relation.
  has_many :ci_pipelines,
259
          -> { ci_sources },
260 261
          class_name: 'Ci::Pipeline',
          inverse_of: :project
262
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
263 264 265 266 267

  # 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.
268
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
269
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
270
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
271
  has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
272
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
273
  has_many :variables, class_name: 'Ci::Variable'
274 275
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
276
  has_many :deployments, -> { success }
277
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
278
  has_many :project_deploy_tokens
279
  has_many :deploy_tokens, through: :project_deploy_tokens
280

281
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
282
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
283

284
  has_many :project_badges, class_name: 'ProjectBadge'
285
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
286

287 288
  has_many :remote_mirrors, inverse_of: :project

289
  accepts_nested_attributes_for :variables, allow_destroy: true
290
  accepts_nested_attributes_for :project_feature, update_only: true
291
  accepts_nested_attributes_for :import_data
292
  accepts_nested_attributes_for :auto_devops, update_only: true
293

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

298 299
  accepts_nested_attributes_for :error_tracking_setting, update_only: true

300
  delegate :name, to: :owner, allow_nil: true, prefix: true
301
  delegate :members, to: :team, prefix: true
302
  delegate :add_user, :add_users, to: :team
303 304
  delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
  delegate :add_master, to: :team # @deprecated
305
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
306 307
  delegate :group_clusters_enabled?, to: :group, allow_nil: true
  delegate :root_ancestor, to: :namespace, allow_nil: true
308
  delegate :last_pipeline, to: :commit, allow_nil: true
309

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
310
  # Validations
311
  validates :creator, presence: true, on: :create
312
  validates :description, length: { maximum: 2000 }, allow_blank: true
313
  validates :ci_config_path,
314
    format: { without: %r{(\.{2}|\A/)},
315
              message: 'cannot include leading slash or directory traversal.' },
316 317
    length: { maximum: 255 },
    allow_blank: true
318 319
  validates :name,
    presence: true,
320
    length: { maximum: 255 },
321
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
322
              message: Gitlab::Regex.project_name_regex_message }
323 324
  validates :path,
    presence: true,
325
    project_path: true,
326
    length: { maximum: 255 }
327

328
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
329
  validates :name, uniqueness: { scope: :namespace_id }
330 331 332
  validates :import_url, public_url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
                                       ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
                                       enforce_user: true }, if: [:external_import?, :import_url_changed?]
333
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
334
  validate :check_personal_projects_limit, on: :create
335
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
336 337
  validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
  validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
338
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
339
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
340 341 342
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
343
  validates :variables, variable_duplicates: { scope: :environment_scope }
344
  validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
345

346
  # Scopes
347
  scope :pending_delete, -> { where(pending_delete: true) }
348
  scope :without_deleted, -> { where(pending_delete: false) }
349

350 351 352
  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) }
353

354 355
  # 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") }
356
  scope :sorted_by_stars, -> { reorder(star_count: :desc) }
357

358
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
359
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
360
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
361
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
362
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
363
  scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
364
  scope :archived, -> { where(archived: true) }
365
  scope :non_archived, -> { where(archived: false) }
366
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
367
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
368
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
369
  scope :with_statistics, -> { includes(:statistics) }
370
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
371 372 373
  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.
374 375
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
376
  end
377 378 379

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
380 381 382 383
    access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
    enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))

    with_project_feature.where(enabled_feature)
384 385 386 387 388 389 390 391
  }

  # 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 })
  }

392 393 394 395 396 397 398 399 400 401
  # Picks projects which use the given programming language
  scope :with_programming_language, ->(language_name) do
    lang_id_query = ProgrammingLanguage
        .with_name_case_insensitive(language_name)
        .select(:id)

    joins(:repository_languages)
        .where(repository_languages: { programming_language_id: lang_id_query })
  end

402 403
  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
404
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
405
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
406
  scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
407

408 409 410 411 412
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

413 414 415 416 417 418
  scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
    subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id')

    where('NOT EXISTS (?)', subquery)
  end

419
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
420

421 422
  chronic_duration_attr :build_timeout_human_readable, :build_timeout,
    default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted'
423 424

  validates :build_timeout, allow_nil: true,
425 426 427 428
                            numericality: { greater_than_or_equal_to: 10.minutes,
                                            less_than: 1.month,
                                            only_integer: true,
                                            message: 'needs to be beetween 10 minutes and 1 month' }
429

430 431 432
  # Used by Projects::CleanupService to hold a map of rewritten object IDs
  mount_uploader :bfg_object_map, AttachmentUploader

433 434 435 436 437 438 439
  # Returns a project, if it is not about to be removed.
  #
  # id - The ID of the project to retrieve.
  def self.find_without_deleted(id)
    without_deleted.find_by_id(id)
  end

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
  # Paginates a collection using a `WHERE id < ?` condition.
  #
  # before - A project ID to use for filtering out projects with an equal or
  #      greater ID. If no ID is given, all projects are included.
  #
  # limit - The maximum number of rows to include.
  def self.paginate_in_descending_order_using_id(
    before: nil,
    limit: Kaminari.config.default_per_page
  )
    relation = order_id_desc.limit(limit)
    relation = relation.where('projects.id < ?', before) if before

    relation
  end

  def self.eager_load_namespace_and_owner
    includes(namespace: :owner)
  end

460 461
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
462 463
  def self.public_or_visible_to_user(user = nil)
    if user
464 465 466
      where('EXISTS (?) OR projects.visibility_level IN (?)',
            user.authorizations_for_projects,
            Gitlab::VisibilityLevel.levels_for_user(user))
467
    else
468
      public_to_user
469 470 471
    end
  end

472
  # project features may be "disabled", "internal", "enabled" or "public". If "internal",
473
  # they are only available to team members. This scope returns projects where
474
  # the feature is either public, enabled, or internal with permission for the user.
475 476 477 478
  #
  # 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.
479
  def self.with_feature_available_for_user(feature, user)
480 481
    visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
    min_access_level = ProjectFeature.required_minimum_access_level(feature)
482 483 484 485 486 487

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

488
      with_project_feature
489 490 491 492 493 494 495 496 497
        .where(
          "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
          " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
          {
            private: Gitlab::VisibilityLevel::PRIVATE,
            public_visible: ProjectFeature::ENABLED,
            private_visible: ProjectFeature::PRIVATE,
            authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
          })
498 499 500
    else
      with_feature_access_level(feature, visible)
    end
501
  end
502

503 504
  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) }
505

506 507
  scope :excluding_project, ->(project) { where.not(id: project) }

508 509
  # We require an alias to the project_mirror_data_table in order to use import_state in our queries
  scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
510
  scope :for_group, -> (group) { where(group: group) }
511

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
512
  class << self
513 514 515 516 517 518 519
    # 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.
520
    def search(query)
521
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
522
    end
523

524
    def search_by_title(query)
525
      non_archived.fuzzy_search(query, [:name])
526 527
    end

528 529 530
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
531

532
    def sort_by_attribute(method)
533 534
      case method.to_s
      when 'storage_size_desc'
535 536 537
        # 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')
538 539 540 541
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
542 543
      when 'stars_desc'
        sorted_by_stars
544 545
      else
        order_by(method)
546 547
      end
    end
548 549

    def reference_pattern
550
      %r{
551
        (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
552 553
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
554
      }x
555
    end
556

557 558 559 560
    def reference_postfix
      '>'
    end

561 562 563 564
    def reference_postfix_escaped
      '&gt;'
    end

565
    # Pattern used to extract `namespace/project>` project references from text.
566 567
    # '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
    # when the reference comes from an external source.
568 569 570
    def markdown_reference_pattern
      %r{
        #{reference_pattern}
571
        (#{reference_postfix}|#{reference_postfix_escaped})
572 573 574
      }x
    end

575
    def trending
576 577
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
578
    end
579 580 581 582 583 584

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

    def group_ids
587
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
588
    end
589 590
  end

591 592 593 594 595 596 597 598
  def all_pipelines
    if builds_enabled?
      super
    else
      super.external
    end
  end

599 600 601 602 603 604 605 606
  def ci_pipelines
    if builds_enabled?
      super
    else
      super.external
    end
  end

607 608
  # returns all ancestor-groups upto but excluding the given namespace
  # when no namespace is given, all ancestors upto the top are returned
609
  def ancestors_upto(top = nil, hierarchy_order: nil)
610
    Gitlab::ObjectHierarchy.new(Group.where(id: namespace_id))
611
      .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
612 613
  end

614 615
  alias_method :ancestors, :ancestors_upto

616
  def lfs_enabled?
617
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
618

619
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
620 621
  end

622 623
  alias_method :lfs_enabled, :lfs_enabled?

624
  def auto_devops_enabled?
625
    if auto_devops&.enabled.nil?
626
      has_auto_devops_implicitly_enabled?
627 628
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
629
    end
630 631
  end

632
  def has_auto_devops_implicitly_enabled?
633 634
    auto_devops&.enabled.nil? &&
      (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
635 636
  end

637
  def has_auto_devops_implicitly_disabled?
638
    auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
639 640
  end

641 642 643 644
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
645
  def team
646
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
647 648 649
  end

  def repository
650
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
651 652
  end

653
  def cleanup
654 655 656
    @repository = nil
  end

657 658
  alias_method :reload_repository!, :cleanup

659
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
660
    if Gitlab.config.registry.enabled
661
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
662
    end
663 664
  end

665
  def has_container_registry_tags?
666 667 668
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
669
      has_root_container_repository_tags?
670 671
  end

672 673
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
674 675
  end

676 677 678 679
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

680 681 682 683
  def commits_by(oids:)
    repository.commits_by(oids: oids)
  end

684
  # ref can't be HEAD, can only be branch/tag name or SHA
685
  def latest_successful_build_for(job_name, ref = default_branch)
686
    latest_pipeline = ci_pipelines.latest_successful_for(ref)
687
    return unless latest_pipeline
688

689
    latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
690 691
  end

692
  def latest_successful_build_for!(job_name, ref = default_branch)
693 694 695
    latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
  end

696
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
697
    sha = repository.merge_base(first_commit_id, second_commit_id)
698
    commit_by(oid: sha) if sha
699 700
  end

701
  def saved?
702
    id && persisted?
703 704
  end

705 706 707 708 709 710 711 712
  def import_status
    import_state&.status || 'none'
  end

  def human_import_status_name
    import_state&.human_status_name || 'none'
  end

713
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
714 715
    job_id =
      if forked?
716
        RepositoryForkWorker.perform_async(id)
717
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
718
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
719
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
720 721 722
      else
        RepositoryImportWorker.perform_async(self.id)
      end
723

724 725 726 727 728 729 730 731
    log_import_activity(job_id)

    job_id
  end

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

732
    if job_id
733
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
734
    else
735
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
736
    end
737 738
  end

739 740 741 742 743
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

744
    import_state.update(last_error: nil)
745 746 747
    remove_import_data
  end

748
  # This method is overridden in EE::Project model
749
  def remove_import_data
750
    import_data&.destroy
751 752
  end

753
  def ci_config_path=(value)
754
    # Strip all leading slashes so that //foo -> foo
755
    super(value&.delete("\0"))
756 757
  end

758
  def import_url=(value)
759 760 761 762 763 764 765
    if Gitlab::UrlSanitizer.valid?(value)
      import_url = Gitlab::UrlSanitizer.new(value)
      super(import_url.sanitized_url)
      create_or_update_import_data(credentials: import_url.credentials)
    else
      super(value)
    end
766 767 768
  end

  def import_url
James Lopez's avatar
James Lopez committed
769
    if import_data && super.present?
770
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
771 772 773
      import_url.full_url
    else
      super
774
    end
775 776
  rescue
    super
777
  end
778

James Lopez's avatar
James Lopez committed
779
  def valid_import_url?
780
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
781 782
  end

783
  def create_or_update_import_data(data: nil, credentials: nil)
784
    return if data.nil? && credentials.nil?
785

James Lopez's avatar
James Lopez committed
786
    project_import_data = import_data || build_import_data
787

788 789
    project_import_data.merge_data(data.to_h)
    project_import_data.merge_credentials(credentials.to_h)
790 791

    project_import_data
792
  end
793

794
  def import?
795
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
796 797 798
  end

  def external_import?
799 800 801
    import_url.present?
  end

802
  def safe_import_url
803
    Gitlab::UrlSanitizer.new(import_url).masked_url
804 805
  end

806 807 808 809
  def bare_repository_import?
    import_type == 'bare_repository'
  end

810 811 812 813
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
814 815 816 817
  def gitea_import?
    import_type == 'gitea'
  end

818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
  def has_remote_mirror?
    remote_mirror_available? && remote_mirrors.enabled.exists?
  end

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

  def update_remote_mirrors
    return unless remote_mirror_available?

    remote_mirrors.enabled.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 mark_remote_mirrors_for_removal
    remote_mirrors.each(&:mark_for_delete_if_blank_url)
  end

  def remote_mirror_available?
    remote_mirror_available_overridden ||
      ::Gitlab::CurrentSettings.mirror_available
  end

849 850 851 852 853 854
  def check_personal_projects_limit
    # Since this method is called as validation hook, `creator` might not be
    # present. Since the validation for that will fail, we can just return
    # early.
    return if !creator || creator.can_create_project? ||
        namespace.kind == 'group'
855

856 857 858 859
    limit = creator.projects_limit
    error =
      if limit.zero?
        _('Personal project creation is not allowed. Please contact your administrator with questions')
860
      else
861
        _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
862
      end
863 864

    self.errors.add(:limit_reached, error % { limit: limit })
gitlabhq's avatar
gitlabhq committed
865 866
  end

867 868 869 870 871 872 873 874 875 876 877 878 879
  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.")
880 881
  end

882 883 884 885 886 887 888 889 890 891
  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
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911
  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

912
  def to_param
913 914 915 916 917
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
918 919
  end

920 921 922 923
  def to_reference_with_postfix
    "#{to_reference(full: true)}#{self.class.reference_postfix}"
  end

924
  # `from` argument can be a Namespace or Project.
925 926
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
927
      full_path
928 929 930
    elsif cross_project_reference?(from)
      path
    end
931 932
  end

933 934
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
935
      name_with_namespace
936
    elsif cross_project_reference?(from)
937 938
      name
    end
939 940
  end

941
  def web_url
942
    Gitlab::Routing.url_helpers.project_url(self)
943 944
  end

945
  def readme_url
946 947 948
    readme_path = repository.readme_path
    if readme_path
      Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme_path))
949 950 951
    end
  end

952
  def new_issuable_address(author, address_type)
953
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
954

955 956 957
    # check since this can come from a request parameter
    return unless %w(issue merge_request).include?(address_type)

958 959
    author.ensure_incoming_email_token!

960 961 962 963 964
    suffix = address_type.dasherize

    # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue@localhost.com
    # example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-merge-request@localhost.com
    Gitlab::IncomingEmail.reply_address("#{full_path_slug}-#{project_id}-#{author.incoming_email_token}-#{suffix}")
965 966
  end

967
  def build_commit_note(commit)
968
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
969
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
970

971
  def last_activity
972
    last_event
gitlabhq's avatar
gitlabhq committed
973 974 975
  end

  def last_activity_date
976
    [last_activity_at, last_repository_updated_at, updated_at].compact.max
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
977
  end
978

979 980 981
  def project_id
    self.id
  end
randx's avatar
randx committed
982

983
  def get_issue(issue_id, current_user)
984 985 986 987 988
    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
989
      ExternalIssue.new(issue_id, self)
990 991 992
    end
  end

Robert Speicher's avatar
Robert Speicher committed
993
  def issue_exists?(issue_id)
994
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
995 996
  end

997
  def default_issue_tracker
998
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
999 1000 1001 1002 1003 1004 1005 1006 1007 1008
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

1009
  def external_issue_reference_pattern
1010
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
1011 1012
  end

1013
  def default_issues_tracker?
1014
    !external_issue_tracker
1015 1016 1017
  end

  def external_issue_tracker
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
    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
1032
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
1033 1034
  end

1035 1036 1037 1038
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
  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
1052
    update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
1053 1054
  end

1055 1056 1057
  def find_or_initialize_services(exceptions: [])
    available_services_names = Service.available_services_names - exceptions

1058
    available_services = available_services_names.map do |service_name|
1059
      find_or_initialize_service(service_name)
1060
    end
1061

1062
    available_services.compact
1063 1064 1065 1066
  end

  def disabled_services
    []
1067 1068
  end

1069
  def find_or_initialize_service(name)
1070 1071
    return if disabled_services.include?(name)

1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
    service = find_service(services, name)
    return service if service

    # We should check if template for the service exists
    template = find_service(services_templates, name)

    if template
      Service.build_from_template(id, template)
    else
      # If no template, we should create an instance. Ex `build_gitlab_ci_service`
      public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
    end
1084 1085
  end

1086
  # rubocop: disable CodeReuse/ServiceClass
1087 1088
  def create_labels
    Label.templates.each do |label|
Felipe Artur's avatar
Felipe Artur committed
1089
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
1090
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
1091 1092
    end
  end
1093
  # rubocop: enable CodeReuse/ServiceClass
1094

1095 1096 1097
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
1098

1099
  def ci_services
1100
    services.where(category: :ci)
1101 1102 1103
  end

  def ci_service
1104
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
1105 1106
  end

1107 1108 1109 1110 1111
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
1112
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
1113 1114
  end

Drew Blessing's avatar
Drew Blessing committed
1115 1116 1117 1118
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

1119
  def avatar_in_git
1120
    repository.avatar
1121 1122
  end

1123
  def avatar_url(**args)
1124
    Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
sue445's avatar
sue445 committed
1125 1126
  end

1127 1128 1129 1130 1131
  # For compatibility with old code
  def code
    path
  end

1132 1133 1134 1135 1136 1137
  def all_clusters
    group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )

    Clusters::Cluster.from_union([clusters, group_clusters])
  end

1138
  def items_for(entity)
1139 1140 1141 1142 1143 1144 1145
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
1146

1147
  # rubocop: disable CodeReuse/ServiceClass
1148
  def send_move_instructions(old_path_with_namespace)
1149 1150
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
1151 1152 1153
    run_after_commit do
      NotificationService.new.project_was_moved(self, old_path_with_namespace)
    end
1154
  end
1155
  # rubocop: enable CodeReuse/ServiceClass
1156 1157

  def owner
1158 1159
    if group
      group
1160
    else
1161
      namespace.try(:owner)
1162 1163
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1164

1165
  # rubocop: disable CodeReuse/ServiceClass
1166
  def execute_hooks(data, hooks_scope = :push_hooks)
1167
    run_after_commit_or_now do
1168
      hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook|
1169 1170
        hook.async_execute(data, hooks_scope.to_s)
      end
1171 1172
      SystemHooksService.new.execute_hooks(data, hooks_scope)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1173
  end
1174
  # rubocop: enable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1175

1176 1177
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
1178 1179 1180 1181
    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
1182 1183 1184 1185
    end
  end

  def valid_repo?
1186
    repository.exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1187
  rescue
1188
    errors.add(:path, 'Invalid repository path')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1189 1190 1191 1192
    false
  end

  def url_to_repo
1193
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1194 1195 1196
  end

  def repo_exists?
1197 1198 1199 1200 1201 1202 1203
    strong_memoize(:repo_exists) do
      begin
        repository.exists?
      rescue
        false
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1204 1205 1206
  end

  def root_ref?(branch)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1207
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1208 1209 1210 1211 1212 1213
  end

  def ssh_url_to_repo
    url_to_repo
  end

1214 1215
  def http_url_to_repo
    "#{web_url}.git"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1216 1217
  end

1218
  # Is overriden in EE
1219 1220 1221 1222
  def lfs_http_url_to_repo(_)
    http_url_to_repo
  end

1223
  def forked?
1224
    fork_network && fork_network.root_project != self
1225
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1226

1227
  def fork_source
1228 1229
    return nil unless forked?

1230 1231 1232
    forked_from_project || fork_network&.root_project
  end

1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244
  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

1245 1246 1247 1248 1249 1250 1251 1252 1253 1254
  # This will return all `lfs_objects` that are accessible to the project.
  # So this might be `self.lfs_objects` if the project is not part of a fork
  # network, or it is the base of the fork network.
  #
  # TODO: refactor this to get the correct lfs objects when implementing
  #       https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
  def all_lfs_objects
    lfs_storage_project.lfs_objects
  end

1255 1256 1257 1258
  def personal?
    !group
  end

1259 1260 1261 1262 1263 1264
  # 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?
1265
      repo.before_delete
1266 1267 1268
    end

    if wiki.exists?
1269
      wiki.before_delete
1270 1271 1272
    end
  end

1273
  # Check if repository already exists on disk
1274 1275
  def check_repository_path_availability
    return true if skip_disk_validation
1276
    return false unless repository_storage
1277

1278 1279 1280
    # 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?
1281 1282 1283 1284 1285
      errors.add(:base, 'There is already a repository with that name on disk')
      return false
    end

    true
1286 1287
  rescue GRPC::Internal # if the path is too long
    false
1288 1289
  end

1290
  def track_project_repository
1291 1292
    repository = project_repository || build_project_repository
    repository.update!(shard_name: repository_storage, disk_path: disk_path)
1293 1294
  end

1295 1296 1297 1298
  def create_repository(force: false)
    # Forked import is handled asynchronously
    return if forked? && !force

1299
    if gitlab_shell.create_project_repository(self)
1300 1301 1302 1303 1304 1305 1306 1307
      repository.after_create
      true
    else
      errors.add(:base, 'Failed to create repository via gitlab-shell')
      false
    end
  end

1308 1309
  def hook_attrs(backward: true)
    attrs = {
1310
      id: id,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1311
      name: name,
1312
      description: description,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
1313
      web_url: web_url,
1314
      avatar_url: avatar_url(only_path: false),
1315 1316
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1317
      namespace: namespace.name,
1318
      visibility_level: visibility_level,
1319
      path_with_namespace: full_path,
1320
      default_branch: default_branch,
1321
      ci_config_path: ci_config_path
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1322
    }
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334

    # 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
1335 1336
  end

1337
  def project_member(user)
1338 1339 1340 1341 1342
    if project_members.loaded?
      project_members.find { |member| member.user_id == user.id }
    else
      project_members.find_by(user_id: user)
    end
1343
  end
1344

1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356
  # Filters `users` to return only authorized users of the project
  def members_among(users)
    if users.is_a?(ActiveRecord::Relation) && !users.loaded?
      authorized_users.merge(users)
    else
      return [] if users.empty?

      user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id)
      users.select { |user| user_ids.include?(user.id) }
    end
  end

1357 1358 1359
  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1360 1361 1362 1363 1364

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1365

1366
  def visibility_level_field
1367
    :visibility_level
1368
  end
1369

1370
  def change_head(branch)
1371 1372
    if repository.branch_exists?(branch)
      repository.before_change_head
1373
      repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}")
1374 1375 1376 1377 1378 1379 1380
      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
1381
  end
1382

1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
  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
1396
  end
1397

1398 1399 1400
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1401

1402
  def ensure_repository
1403
    create_repository(force: true) unless repository_exists?
1404 1405
  end

1406 1407 1408 1409
  def repository_exists?
    !!repository.exists?
  end

1410 1411 1412 1413
  def wiki_repository_exists?
    wiki.repository_exists?
  end

1414
  # update visibility_level of forks
1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425
  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

1426 1427 1428
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
1429
  rescue ProjectWiki::CouldNotCreateWikiError
1430
    errors.add(:base, 'Failed create wiki')
1431 1432
    false
  end
1433

1434 1435 1436 1437
  def wiki
    @wiki ||= ProjectWiki.new(self, self.owner)
  end

Drew Blessing's avatar
Drew Blessing committed
1438 1439 1440 1441
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end

1442
  def allowed_to_share_with_group?
1443
    !namespace.share_with_group_lock
1444 1445
  end

1446 1447 1448
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1449
    return unless sha
1450

1451
    ci_pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
1452 1453
  end

1454 1455 1456 1457 1458 1459
  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 =
1460
      ci_pipelines.latest_successful_for(default_branch)
1461 1462 1463 1464
  end

  def latest_successful_pipeline_for(ref = nil)
    if ref && ref != default_branch
1465
      ci_pipelines.latest_successful_for(ref)
1466 1467 1468 1469 1470
    else
      latest_successful_pipeline_for_default_branch
    end
  end

1471
  def enable_ci
1472
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1473
  end
Marin Jankovski's avatar
Marin Jankovski committed
1474

1475 1476 1477 1478 1479
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1480
    @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
1481 1482
  end

1483
  def group_runners
1484
    @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
1485 1486
  end

1487
  def all_runners
1488
    Ci::Runner.from_union([runners, group_runners, shared_runners])
1489
  end
1490

1491 1492 1493 1494 1495
  def active_runners
    strong_memoize(:active_runners) do
      all_runners.active
    end
  end
1496

1497 1498
  def any_runners?(&block)
    active_runners.any?(&block)
1499 1500
  end

1501
  def valid_runners_token?(token)
James Lopez's avatar
James Lopez committed
1502
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1503 1504
  end

1505
  # rubocop: disable CodeReuse/ServiceClass
Felipe Artur's avatar
Felipe Artur committed
1506 1507
  def open_issues_count(current_user = nil)
    Projects::OpenIssuesCountService.new(self, current_user).count
1508
  end
1509
  # rubocop: enable CodeReuse/ServiceClass
1510

1511
  # rubocop: disable CodeReuse/ServiceClass
1512 1513
  def open_merge_requests_count
    Projects::OpenMergeRequestsCountService.new(self).count
1514
  end
1515
  # rubocop: enable CodeReuse/ServiceClass
1516

1517
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
Douwe Maan's avatar
Douwe Maan committed
1518
    return true unless forked?
1519

1520
    original_project = fork_source
Douwe Maan's avatar
Douwe Maan committed
1521 1522 1523
    return true unless original_project

    level <= original_project.visibility_level
1524
  end
1525

1526 1527
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1528

1529
    level <= group.visibility_level
Marin Jankovski's avatar
Marin Jankovski committed
1530
  end
1531

1532 1533
  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
1534 1535
  end

1536 1537 1538
  def runners_token
    ensure_runners_token!
  end
1539

1540 1541 1542
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1543

1544
  def pages_group_url
1545
    # The host in URL always needs to be downcased
1546 1547
    Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
      "#{prefix}#{pages_subdomain}."
1548
    end.downcase
1549 1550 1551 1552 1553
  end

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

1555
    # If the project path is the same as host, we serve it as group page
1556
    return url if url == "#{Settings.pages.protocol}://#{url_path}"
1557 1558 1559

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

1561 1562
  def pages_subdomain
    full_path.partition('/').first
1563
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1564 1565

  def pages_path
1566 1567
    # 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
1568 1569 1570 1571 1572 1573
  end

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

1574
  def pages_available?
1575
    Gitlab.config.pages.enabled
1576 1577
  end

1578
  def remove_private_deploy_keys
1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590
    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
1591 1592
  end

1593
  # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
1594
  # rubocop: disable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1595
  def remove_pages
1596 1597 1598
    # Projects with a missing namespace cannot have their pages removed
    return unless namespace

1599 1600
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1601 1602 1603
    # 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
1604
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
1605

1606 1607
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
1608
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1609
  end
1610
  # rubocop: enable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1611

1612 1613 1614 1615
  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.
1616
    repository.raw_repository.write_config(full_path: gl_full_path)
1617
  rescue Gitlab::Git::Repository::NoRepository => e
1618
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
1619
    nil
1620 1621
  end

1622 1623
  def after_import
    repository.after_import
1624
    wiki.repository.after_import
1625 1626 1627 1628 1629 1630 1631

    # The import assigns iid values on its own, e.g. by re-using GitHub ids.
    # Flush existing InternalId records for this project for consistency reasons.
    # Those records are going to be recreated with the next normal creation
    # of a model instance (e.g. an Issue).
    InternalId.flush_records!(project: self)

1632 1633
    import_state.finish
    import_state.remove_jid
1634
    update_project_counter_caches
1635
    after_create_default_branch
1636
    join_pool_repository
1637
    refresh_markdown_cache!
1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650
  end

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

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

1651
  # rubocop: disable CodeReuse/ServiceClass
1652
  def after_create_default_branch
1653
    Projects::ProtectDefaultBranchService.new(self).execute
1654
  end
1655
  # rubocop: enable CodeReuse/ServiceClass
1656

1657
  # Lazy loading of the `pipeline_status` attribute
1658
  def pipeline_status
1659
    @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
1660 1661
  end

1662 1663
  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)
1664 1665 1666 1667 1668 1669 1670

    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
1671

1672 1673
  def import_export_shared
    @import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
1674 1675
  end

James Lopez's avatar
James Lopez committed
1676
  def export_path
1677 1678
    return nil unless namespace.present? || hashed_storage?(:repository)

1679
    import_export_shared.archive_path
James Lopez's avatar
James Lopez committed
1680
  end
1681

1682 1683 1684
  def export_status
    if export_in_progress?
      :started
1685 1686
    elsif after_export_in_progress?
      :after_export_action
1687
    elsif export_file_exists?
1688 1689 1690 1691 1692 1693 1694
      :finished
    else
      :none
    end
  end

  def export_in_progress?
1695
    import_export_shared.active_export_count > 0
1696 1697
  end

1698 1699 1700 1701
  def after_export_in_progress?
    import_export_shared.after_export_in_progress?
  end

1702
  def remove_exports
1703
    return unless export_file_exists?
1704

1705
    import_export_upload.remove_export_file!
1706
    import_export_upload.save unless import_export_upload.destroyed?
1707
  end
1708

1709
  def export_file_exists?
1710
    export_file&.file
1711
  end
1712

1713 1714
  def export_file
    import_export_upload&.export_file
1715 1716
  end

1717 1718 1719 1720
  def full_path_slug
    Gitlab::Utils.slugify(full_path.to_s)
  end

1721
  def has_ci?
1722
    repository.gitlab_ci_yml || auto_devops_enabled?
1723 1724
  end

1725
  def predefined_variables
1726 1727
    visibility = Gitlab::VisibilityLevel.string_level(visibility_level)

1728 1729 1730 1731 1732 1733 1734 1735
    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)
1736
      .concat(pages_variables)
1737 1738
      .concat(container_registry_variables)
      .concat(auto_devops_variables)
1739
      .concat(api_variables)
1740 1741
  end

1742 1743 1744 1745 1746 1747 1748
  def pages_variables
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
      variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host)
      variables.append(key: 'CI_PAGES_URL', value: pages_url)
    end
  end

1749 1750
  def api_variables
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1751
      variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url)
1752
    end
1753 1754 1755
  end

  def container_registry_variables
1756
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1757
      break variables unless Gitlab.config.registry.enabled
1758

1759
      variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
1760

1761
      if container_registry_enabled?
1762
        variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url)
1763
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1764
    end
1765 1766
  end

1767 1768 1769 1770 1771 1772 1773 1774 1775
  def default_environment
    production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"

    environments
      .with_state(:available)
      .reorder(production_first)
      .first
  end

1776
  def ci_variables_for(ref:, environment: nil)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
1777
    # EE would use the environment
1778 1779 1780 1781 1782 1783
    if protected_for?(ref)
      variables
    else
      variables.unprotected
    end
  end
1784

1785
  def protected_for?(ref)
1786
    raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
1787

1788
    resolved_ref = repository.expand_ref(ref) || ref
1789 1790
    return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)

1791 1792 1793 1794 1795
    ref_name = if resolved_ref == ref
                 Gitlab::Git.ref_name(resolved_ref)
               else
                 ref
               end
1796 1797 1798 1799 1800

    if Gitlab::Git.branch_ref?(resolved_ref)
      ProtectedBranch.protected?(self, ref_name)
    elsif Gitlab::Git.tag_ref?(resolved_ref)
      ProtectedTag.protected?(self, ref_name)
1801
    end
1802
  end
1803

1804
  def deployment_variables(environment: nil)
1805
    deployment_platform(environment: environment)&.predefined_variables(project: self) || []
1806 1807
  end

1808 1809 1810
  def auto_devops_variables
    return [] unless auto_devops_enabled?

1811
    (auto_devops || build_auto_devops)&.predefined_variables
1812 1813
  end

1814
  def append_or_update_attribute(name, value)
1815
    old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
1816 1817 1818 1819 1820 1821

    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
1822 1823 1824

  rescue ActiveRecord::RecordNotSaved => e
    handle_update_attribute_error(e, value)
1825 1826
  end

1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844
  # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
  #
  # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
  def set_repository_read_only!
    with_lock do
      break false if git_transfer_in_progress?

      update_column(:repository_read_only, true)
    end
  end

  # Set repository as writable again
  def set_repository_writable!
    with_lock do
      update_column(repository_read_only, false)
    end
  end

1845
  def pushes_since_gc
1846
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
1847 1848 1849
  end

  def increment_pushes_since_gc
1850
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
1851 1852 1853
  end

  def reset_pushes_since_gc
1854
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
1855 1856
  end

Douwe Maan's avatar
Douwe Maan committed
1857
  def route_map_for(commit_sha)
1858 1859
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
Douwe Maan's avatar
Douwe Maan committed
1860
        data = repository.route_map_for(sha)
1861 1862
        next unless data

Douwe Maan's avatar
Douwe Maan committed
1863 1864 1865
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1866 1867 1868 1869 1870 1871 1872
      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
1873
    map = route_map_for(commit_sha)
1874 1875
    return unless map

Douwe Maan's avatar
Douwe Maan committed
1876
    map.public_path_for_source_path(path)
1877 1878
  end

1879 1880 1881 1882
  def parent_changed?
    namespace_id_changed?
  end

1883 1884 1885 1886 1887 1888 1889 1890
  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
1891 1892 1893
  # Overridden on EE module
  def multiple_issue_boards_available?
    false
Felipe Artur's avatar
Felipe Artur committed
1894 1895
  end

1896 1897 1898 1899
  def full_path_was
    File.join(namespace.full_path, previous_changes['path'].first)
  end

1900 1901
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
1902
  # @deprecated cannot remove yet because it has an index with its name in elasticsearch
1903 1904
  alias_method :path_with_namespace, :full_path

1905
  # rubocop: disable CodeReuse/ServiceClass
1906 1907 1908
  def forks_count
    Projects::ForksCountService.new(self).count
  end
1909
  # rubocop: enable CodeReuse/ServiceClass
1910

1911
  def legacy_storage?
1912 1913 1914
    [nil, 0].include?(self.storage_version)
  end

1915 1916 1917 1918
  # 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)
1919 1920 1921
    raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)

    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
1922 1923
  end

1924 1925 1926 1927
  def renamed?
    persisted? && path_changed?
  end

1928 1929 1930
  def merge_method
    if self.merge_requests_ff_only_enabled
      :ff
1931 1932
    elsif self.merge_requests_rebase_enabled
      :rebase_merge
1933 1934 1935 1936 1937 1938
    else
      :merge
    end
  end

  def merge_method=(method)
1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949
    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
1950 1951 1952
  end

  def ff_merge_must_be_possible?
1953
    self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
1954 1955
  end

1956
  def migrate_to_hashed_storage!
1957
    return unless storage_upgradable?
1958

1959
    if git_transfer_in_progress?
1960 1961 1962 1963 1964 1965
      ProjectMigrateHashedStorageWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
    else
      ProjectMigrateHashedStorageWorker.perform_async(id)
    end
  end

1966 1967 1968 1969
  def git_transfer_in_progress?
    repo_reference_count > 0 || wiki_reference_count > 0
  end

1970 1971 1972 1973 1974 1975 1976 1977 1978 1979
  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

1980 1981 1982 1983
  def reference_counter(wiki: false)
    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
  end

1984 1985 1986
  def badges
    return project_badges unless group

1987 1988 1989 1990
    Badge.from_union([
      project_badges,
      GroupBadge.where(group: group.self_and_ancestors)
    ])
1991 1992
  end

1993 1994 1995 1996 1997 1998 1999 2000
  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)
2001 2002 2003 2004
    merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
  end

  def any_branch_allows_collaboration?(user)
2005
    fetch_branch_allows_collaboration(user)
2006 2007
  end

2008
  def branch_allows_collaboration?(user, branch_name)
2009
    fetch_branch_allows_collaboration(user, branch_name)
2010 2011
  end

2012 2013 2014 2015
  def licensed_features
    []
  end

2016 2017
  def toggle_ci_cd_settings!(settings_attribute)
    ci_cd_settings.toggle!(settings_attribute)
2018 2019
  end

2020
  def gitlab_deploy_token
2021
    @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
2022 2023
  end

2024 2025 2026 2027 2028
  def any_lfs_file_locks?
    lfs_file_locks.any?
  end
  request_cache(:any_lfs_file_locks?) { self.id }

2029 2030 2031 2032
  def auto_cancel_pending_pipelines?
    auto_cancel_pending_pipelines == 'enabled'
  end

2033 2034
  def storage
    @storage ||=
2035
      if hashed_storage?(:repository)
2036 2037 2038 2039 2040
        Storage::HashedProject.new(self)
      else
        Storage::LegacyProject.new(self)
      end
  end
2041

2042 2043 2044 2045
  def storage_upgradable?
    storage_version != LATEST_STORAGE_VERSION
  end

2046 2047 2048 2049
  def snippets_visible?(user = nil)
    Ability.allowed?(user, :read_project_snippet, self)
  end

2050 2051 2052 2053
  def max_attachment_size
    Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
  end

2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076
  def object_pool_params
    return {} unless !forked? && git_objects_poolable?

    {
      repository_storage: repository_storage,
      pool_repository:    pool_repository || create_new_pool_repository
    }
  end

  # Git objects are only poolable when the project is or has:
  # - Hashed storage -> The object pool will have a remote to its members, using relative paths.
  #                     If the repository path changes we would have to update the remote.
  # - Public         -> User will be able to fetch Git objects that might not exist
  #                     in their own repository.
  # - Repository     -> Else the disk path will be empty, and there's nothing to pool
  def git_objects_poolable?
    hashed_storage?(:repository) &&
      public? &&
      repository_exists? &&
      Gitlab::CurrentSettings.hashed_storage_enabled &&
      Feature.enabled?(:object_pools, self)
  end

2077
  def leave_pool_repository
2078
    pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil)
2079 2080
  end

2081 2082 2083 2084
  def link_pool_repository
    pool_repository&.link_repository(repository)
  end

2085 2086 2087 2088
  def has_pool_repository?
    pool_repository.present?
  end

2089 2090
  private

2091 2092 2093 2094
  def merge_requests_allowing_collaboration(source_branch = nil)
    relation = source_of_merge_requests.opened.where(allow_collaboration: true)
    relation = relation.where(source_branch: source_branch) if source_branch
    relation
2095 2096
  end

2097 2098
  def create_new_pool_repository
    pool = begin
2099
             create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
2100
           rescue ActiveRecord::RecordNotUnique
2101
             pool_repository(true)
2102 2103
           end

2104
    pool.schedule unless pool.scheduled?
2105 2106 2107 2108 2109 2110 2111 2112 2113
    pool
  end

  def join_pool_repository
    return unless pool_repository

    ObjectPool::JoinWorker.perform_async(pool_repository.id, self.id)
  end

2114
  def use_hashed_storage
2115
    if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
2116
      self.storage_version = LATEST_STORAGE_VERSION
2117 2118 2119
    end
  end

2120
  def repo_reference_count
2121
    reference_counter.value
2122 2123 2124
  end

  def wiki_reference_count
2125
    reference_counter(wiki: true).value
2126 2127
  end

2128 2129 2130
  def check_repository_absence!
    return if skip_disk_validation

2131
    if repository_storage.blank? || repository_with_same_path_already_exists?
2132 2133 2134 2135 2136 2137
      errors.add(:base, 'There is already a repository with that name on disk')
      throw :abort
    end
  end

  def repository_with_same_path_already_exists?
2138
    gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
2139 2140
  end

2141 2142
  def set_timestamps_for_create
    update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at)
2143 2144
  end

2145
  def cross_namespace_reference?(from)
2146 2147 2148 2149 2150
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
2151 2152 2153
    end
  end

2154
  # Check if a reference is being done cross-project
2155 2156 2157 2158
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
2159 2160
  end

2161
  def pushes_since_gc_redis_shared_state_key
2162 2163 2164
    "projects/#{id}/pushes_since_gc"
  end

2165 2166 2167 2168 2169 2170 2171
  # 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.
2172
  def validate_board_limit(board)
2173
    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
2174
  end
2175

2176 2177 2178 2179
  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194

  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

2195
    Project.pending_delete.find_by_full_path(full_path)
2196
  end
2197 2198 2199 2200 2201 2202 2203 2204 2205

  ##
  # 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

2206
    ContainerRepository.build_root_repository(self).has_tags?
2207
  end
2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219

  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
2220

2221
  def fetch_branch_allows_collaboration(user, branch_name = nil)
2222 2223
    return false unless user

2224
    Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
2225 2226
      next false if empty_repo?

2227 2228 2229 2230 2231 2232
      # Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322
      Gitlab::GitalyClient.allow_n_plus_1_calls do
        merge_requests_allowing_collaboration(branch_name).any? do |merge_request|
          merge_request.can_be_merged_by?(user)
        end
      end
2233 2234
    end
  end
2235 2236 2237 2238

  def services_templates
    @services_templates ||= Service.where(template: true)
  end
gitlabhq's avatar
gitlabhq committed
2239
end