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

3 4
require 'carrierwave/orm/activerecord'

5
class Project < ApplicationRecord
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
Douwe Maan's avatar
Douwe Maan committed
41
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
42 43
  # Hashed Storage versions handle rolling out new storage to project and dependents models:
  # nil: legacy
44 45 46
  # 1: repository
  # 2: attachments
  LATEST_STORAGE_VERSION = 2
47 48 49 50
  HASHED_STORAGE_FEATURES = {
    repository: 1,
    attachments: 2
  }.freeze
Jared Szechy's avatar
Jared Szechy committed
51

52 53 54 55 56
  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
57

58 59
  ignore_column :import_status, :import_jid, :import_error

60 61
  cache_markdown_field :description, pipeline: :description

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

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

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

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

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

87
  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
88

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

91
  before_save :ensure_runners_token
92

Jasper Maes's avatar
Jasper Maes committed
93
  after_save :update_project_statistics, if: :saved_change_to_namespace_id?
94 95 96

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

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

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

103
  after_create :set_timestamps_for_create
104
  after_update :update_forks_visibility_level
105

106
  before_destroy :remove_private_deploy_keys
107

108
  use_fast_destroy :build_trace_chunks
109

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

113 114
  after_validation :check_pending_delete

115
  # Storage specific hooks
116
  after_initialize :use_hashed_storage
117
  after_create :check_repository_absence!
118
  after_create :ensure_storage_path_exists
Jasper Maes's avatar
Jasper Maes committed
119
  after_save :ensure_storage_path_exists, if: :saved_change_to_namespace_id?
120

121
  acts_as_ordered_taggable
122

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

128 129
  alias_attribute :title, :name

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

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

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

177 178 179 180 181
  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
182
  has_one :fork_network, through: :fork_network_member
183 184 185
  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
186

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

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

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

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

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

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

233 234
  has_many :internal_ids

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

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

244 245
  has_many :prometheus_metrics

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

251
  has_many :commit_statuses
252
  # The relation :all_pipelines is intended to be used when we want to get the
253 254
  # whole list of pipelines associated to the project
  has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
255
  # The relation :ci_pipelines is intended to be used when we want to get only
256 257 258 259
  # 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,
260
          -> { ci_sources },
261 262
          class_name: 'Ci::Pipeline',
          inverse_of: :project
263
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
264 265 266 267 268

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

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

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

288 289
  has_many :remote_mirrors, inverse_of: :project

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

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

299 300
  accepts_nested_attributes_for :error_tracking_setting, update_only: true

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

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

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

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

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

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

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

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
381 382 383 384
    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)
385 386 387 388 389 390 391 392
  }

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

393 394 395 396 397 398 399 400 401 402
  # 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

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

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

414 415 416 417 418 419
  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

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

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

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

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

434 435 436 437 438 439 440
  # 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

441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
  # 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

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

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

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

489
      with_project_feature
490 491 492 493 494 495 496 497 498
        .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)
          })
499 500 501
    else
      with_feature_access_level(feature, visible)
    end
502
  end
503

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

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

509 510
  # 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") }
511
  scope :for_group, -> (group) { where(group: group) }
512

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

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

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

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

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

558 559 560 561
    def reference_postfix
      '>'
    end

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

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

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

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

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

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

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

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

615 616
  alias_method :ancestors, :ancestors_upto

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

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

623 624
  alias_method :lfs_enabled, :lfs_enabled?

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

633
  def has_auto_devops_implicitly_enabled?
634 635 636
    auto_devops_config = first_auto_devops_config

    auto_devops_config[:scope] != :project && auto_devops_config[:status]
637 638
  end

639
  def has_auto_devops_implicitly_disabled?
640 641 642 643 644 645 646 647 648
    auto_devops_config = first_auto_devops_config

    auto_devops_config[:scope] != :project && !auto_devops_config[:status]
  end

  def first_auto_devops_config
    return namespace.first_auto_devops_config if auto_devops&.enabled.nil?

    { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
649 650
  end

651 652 653 654
  def multiple_mr_assignees_enabled?
    Feature.enabled?(:multiple_merge_request_assignees, self)
  end

655 656 657 658
  def daily_statistics_enabled?
    Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
  end

659 660 661 662
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
663
  def team
664
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
665 666 667
  end

  def repository
668
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
669 670
  end

671
  def cleanup
672 673 674
    @repository = nil
  end

675 676
  alias_method :reload_repository!, :cleanup

677
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
678
    if Gitlab.config.registry.enabled
679
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
680
    end
681 682
  end

683
  def has_container_registry_tags?
684 685 686
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
687
      has_root_container_repository_tags?
688 689
  end

690 691
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
692 693
  end

694 695 696 697
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

698 699 700 701
  def commits_by(oids:)
    repository.commits_by(oids: oids)
  end

702
  # ref can't be HEAD, can only be branch/tag name or SHA
703
  def latest_successful_build_for(job_name, ref = default_branch)
704
    latest_pipeline = ci_pipelines.latest_successful_for(ref)
705
    return unless latest_pipeline
706

707
    latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
708 709
  end

710
  def latest_successful_build_for!(job_name, ref = default_branch)
711 712 713
    latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
  end

714
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
715
    sha = repository.merge_base(first_commit_id, second_commit_id)
716
    commit_by(oid: sha) if sha
717 718
  end

719
  def saved?
720
    id && persisted?
721 722
  end

723 724 725 726 727 728 729 730
  def import_status
    import_state&.status || 'none'
  end

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

731
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
732 733
    job_id =
      if forked?
734
        RepositoryForkWorker.perform_async(id)
735
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
736
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
737
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
738 739 740
      else
        RepositoryImportWorker.perform_async(self.id)
      end
741

742 743 744 745 746 747 748 749
    log_import_activity(job_id)

    job_id
  end

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

750
    if job_id
751
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
752
    else
753
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
754
    end
755 756
  end

757 758 759 760 761
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

762
    import_state.update(last_error: nil)
763 764 765
    remove_import_data
  end

766
  # This method is overridden in EE::Project model
767
  def remove_import_data
768
    import_data&.destroy
769 770
  end

771
  def ci_config_path=(value)
772
    # Strip all leading slashes so that //foo -> foo
773
    super(value&.delete("\0"))
774 775
  end

776
  def import_url=(value)
777 778 779 780 781 782 783
    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
784 785 786
  end

  def import_url
James Lopez's avatar
James Lopez committed
787
    if import_data && super.present?
788
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
789 790 791
      import_url.full_url
    else
      super
792
    end
793 794
  rescue
    super
795
  end
796

James Lopez's avatar
James Lopez committed
797
  def valid_import_url?
798
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
799 800
  end

801
  def create_or_update_import_data(data: nil, credentials: nil)
802
    return if data.nil? && credentials.nil?
803

James Lopez's avatar
James Lopez committed
804
    project_import_data = import_data || build_import_data
805

806 807
    project_import_data.merge_data(data.to_h)
    project_import_data.merge_credentials(credentials.to_h)
808 809

    project_import_data
810
  end
811

812
  def import?
813
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
814 815 816
  end

  def external_import?
817 818 819
    import_url.present?
  end

820
  def safe_import_url
821
    Gitlab::UrlSanitizer.new(import_url).masked_url
822 823
  end

824 825 826 827
  def bare_repository_import?
    import_type == 'bare_repository'
  end

828 829 830 831
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
832 833 834 835
  def gitea_import?
    import_type == 'gitea'
  end

836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
  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,
853
      last_error: _('The remote mirror took to long to complete.'),
854 855 856 857 858 859 860 861 862 863 864 865 866
      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

867 868 869 870 871 872
  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'
873

874 875 876 877
    limit = creator.projects_limit
    error =
      if limit.zero?
        _('Personal project creation is not allowed. Please contact your administrator with questions')
878
      else
879
        _('Your project limit is %{limit} projects! Please contact your administrator to increase it')
880
      end
881 882

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

885 886 887 888 889
  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
890
    self.errors.add(:visibility_level, _("%{level_name} is not allowed in a %{group_level_name} group.") % { level_name: level_name, group_level_name: group_level_name })
891 892 893 894 895 896
  end

  def visibility_level_allowed_as_fork
    return if visibility_level_allowed_as_fork?

    level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
897
    self.errors.add(:visibility_level, _("%{level_name} is not allowed since the fork source project has lower visibility.") % { level_name: level_name })
898 899
  end

900 901 902 903 904 905
  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?
906
      errors.add(:name, _('has already been taken'))
907 908 909
    end
  end

Rob Watson's avatar
Rob Watson committed
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925
  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?)
926
      errors.add(:pages_https_only, _("cannot be enabled unless all domains have TLS certificates"))
Rob Watson's avatar
Rob Watson committed
927 928 929
    end
  end

930
  def to_param
931 932 933 934 935
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
936 937
  end

938 939 940 941
  def to_reference_with_postfix
    "#{to_reference(full: true)}#{self.class.reference_postfix}"
  end

942
  # `from` argument can be a Namespace or Project.
943 944
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
945
      full_path
946 947 948
    elsif cross_project_reference?(from)
      path
    end
949 950
  end

951 952
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
953
      name_with_namespace
954
    elsif cross_project_reference?(from)
955 956
      name
    end
957 958
  end

959
  def web_url
960
    Gitlab::Routing.url_helpers.project_url(self)
961 962
  end

963
  def readme_url
964 965 966
    readme_path = repository.readme_path
    if readme_path
      Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme_path))
967 968 969
    end
  end

970
  def new_issuable_address(author, address_type)
971
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
972

973 974 975
    # check since this can come from a request parameter
    return unless %w(issue merge_request).include?(address_type)

976 977
    author.ensure_incoming_email_token!

978 979 980 981 982
    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}")
983 984
  end

985
  def build_commit_note(commit)
986
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
987
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
988

989
  def last_activity
990
    last_event
gitlabhq's avatar
gitlabhq committed
991 992 993
  end

  def last_activity_date
994
    [last_activity_at, last_repository_updated_at, updated_at].compact.max
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
995
  end
996

997 998 999
  def project_id
    self.id
  end
randx's avatar
randx committed
1000

1001
  def get_issue(issue_id, current_user)
1002 1003 1004 1005 1006
    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
1007
      ExternalIssue.new(issue_id, self)
1008 1009 1010
    end
  end

Robert Speicher's avatar
Robert Speicher committed
1011
  def issue_exists?(issue_id)
1012
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
1013 1014
  end

1015
  def default_issue_tracker
1016
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

1027
  def external_issue_reference_pattern
1028
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
1029 1030
  end

1031
  def default_issues_tracker?
1032
    !external_issue_tracker
1033 1034 1035
  end

  def external_issue_tracker
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
    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
1050
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
1051 1052
  end

1053 1054 1055 1056
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
  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
1070
    update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
1071 1072
  end

1073 1074 1075
  def find_or_initialize_services(exceptions: [])
    available_services_names = Service.available_services_names - exceptions

1076
    available_services = available_services_names.map do |service_name|
1077
      find_or_initialize_service(service_name)
1078
    end
1079

1080
    available_services.compact
1081 1082 1083 1084
  end

  def disabled_services
    []
1085 1086
  end

1087
  def find_or_initialize_service(name)
1088 1089
    return if disabled_services.include?(name)

1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101
    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
1102 1103
  end

1104
  # rubocop: disable CodeReuse/ServiceClass
1105 1106
  def create_labels
    Label.templates.each do |label|
Felipe Artur's avatar
Felipe Artur committed
1107
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
1108
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
1109 1110
    end
  end
1111
  # rubocop: enable CodeReuse/ServiceClass
1112

1113 1114 1115
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
1116

1117
  def ci_services
1118
    services.where(category: :ci)
1119 1120 1121
  end

  def ci_service
1122
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
1123 1124
  end

1125 1126 1127 1128 1129
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
1130
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
1131 1132
  end

Drew Blessing's avatar
Drew Blessing committed
1133 1134 1135 1136
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

1137
  def avatar_in_git
1138
    repository.avatar
1139 1140
  end

1141
  def avatar_url(**args)
1142
    Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
sue445's avatar
sue445 committed
1143 1144
  end

1145 1146 1147 1148 1149
  # For compatibility with old code
  def code
    path
  end

1150 1151 1152 1153 1154 1155
  def all_clusters
    group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )

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

1156
  def items_for(entity)
1157 1158 1159 1160 1161 1162 1163
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
1164

1165
  # rubocop: disable CodeReuse/ServiceClass
1166
  def send_move_instructions(old_path_with_namespace)
1167 1168
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
1169 1170 1171
    run_after_commit do
      NotificationService.new.project_was_moved(self, old_path_with_namespace)
    end
1172
  end
1173
  # rubocop: enable CodeReuse/ServiceClass
1174 1175

  def owner
1176 1177
    if group
      group
1178
    else
1179
      namespace.try(:owner)
1180 1181
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1182

1183
  # rubocop: disable CodeReuse/ServiceClass
1184
  def execute_hooks(data, hooks_scope = :push_hooks)
1185
    run_after_commit_or_now do
1186
      hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook|
1187 1188
        hook.async_execute(data, hooks_scope.to_s)
      end
1189 1190
      SystemHooksService.new.execute_hooks(data, hooks_scope)
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1191
  end
1192
  # rubocop: enable CodeReuse/ServiceClass
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1193

1194 1195
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
1196 1197 1198 1199
    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
1200 1201 1202 1203
    end
  end

  def valid_repo?
1204
    repository.exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1205
  rescue
1206
    errors.add(:path, _('Invalid repository path'))
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1207 1208 1209 1210
    false
  end

  def url_to_repo
1211
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1212 1213 1214
  end

  def repo_exists?
1215
    strong_memoize(:repo_exists) do
Nick Thomas's avatar
Nick Thomas committed
1216 1217 1218
      repository.exists?
    rescue
      false
1219
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1220 1221 1222
  end

  def root_ref?(branch)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1223
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1224 1225 1226 1227 1228 1229
  end

  def ssh_url_to_repo
    url_to_repo
  end

1230 1231
  def http_url_to_repo
    "#{web_url}.git"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1232 1233
  end

1234
  # Is overridden in EE
1235 1236 1237 1238
  def lfs_http_url_to_repo(_)
    http_url_to_repo
  end

1239
  def forked?
1240
    fork_network && fork_network.root_project != self
1241
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1242

1243
  def fork_source
1244
    return unless forked?
1245

1246 1247 1248
    forked_from_project || fork_network&.root_project
  end

1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
  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

1261 1262 1263 1264 1265 1266 1267 1268 1269 1270
  # 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

1271 1272 1273 1274
  def personal?
    !group
  end

1275 1276 1277 1278 1279 1280
  # 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?
1281
      repo.before_delete
1282 1283 1284
    end

    if wiki.exists?
1285
      wiki.before_delete
1286 1287 1288
    end
  end

1289
  # Check if repository already exists on disk
1290 1291
  def check_repository_path_availability
    return true if skip_disk_validation
1292
    return false unless repository_storage
1293

1294 1295 1296
    # 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?
1297
      errors.add(:base, _('There is already a repository with that name on disk'))
1298 1299 1300 1301
      return false
    end

    true
1302 1303
  rescue GRPC::Internal # if the path is too long
    false
1304 1305
  end

1306
  def track_project_repository
1307 1308
    repository = project_repository || build_project_repository
    repository.update!(shard_name: repository_storage, disk_path: disk_path)
1309 1310
  end

1311 1312 1313 1314
  def create_repository(force: false)
    # Forked import is handled asynchronously
    return if forked? && !force

1315
    if gitlab_shell.create_project_repository(self)
1316 1317 1318
      repository.after_create
      true
    else
1319
      errors.add(:base, _('Failed to create repository via gitlab-shell'))
1320 1321 1322 1323
      false
    end
  end

1324 1325
  def hook_attrs(backward: true)
    attrs = {
1326
      id: id,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1327
      name: name,
1328
      description: description,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
1329
      web_url: web_url,
1330
      avatar_url: avatar_url(only_path: false),
1331 1332
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1333
      namespace: namespace.name,
1334
      visibility_level: visibility_level,
1335
      path_with_namespace: full_path,
1336
      default_branch: default_branch,
1337
      ci_config_path: ci_config_path
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1338
    }
1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350

    # 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
1351 1352
  end

1353
  def project_member(user)
1354 1355 1356 1357 1358
    if project_members.loaded?
      project_members.find { |member| member.user_id == user.id }
    else
      project_members.find_by(user_id: user)
    end
1359
  end
1360

1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372
  # 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

1373 1374 1375
  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1376 1377 1378 1379 1380

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1381

1382
  def visibility_level_field
1383
    :visibility_level
1384
  end
1385

1386
  def change_head(branch)
1387 1388
    if repository.branch_exists?(branch)
      repository.before_change_head
1389
      repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}")
1390 1391
      repository.copy_gitattributes(branch)
      repository.after_change_head
1392
      ProjectCacheWorker.perform_async(self.id, [], [:commit_count])
1393 1394
      reload_default_branch
    else
1395
      errors.add(:base, _("Could not change HEAD: branch '%{branch}' does not exist") % { branch: branch })
1396 1397
      false
    end
1398
  end
1399

1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412
  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
1413
  end
1414

1415 1416 1417
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1418

1419
  def ensure_repository
1420
    create_repository(force: true) unless repository_exists?
1421 1422
  end

1423 1424 1425 1426
  def repository_exists?
    !!repository.exists?
  end

1427 1428 1429 1430
  def wiki_repository_exists?
    wiki.repository_exists?
  end

1431
  # update visibility_level of forks
1432
  def update_forks_visibility_level
Jasper Maes's avatar
Jasper Maes committed
1433
    return unless visibility_level < visibility_level_before_last_save
1434 1435 1436 1437 1438 1439 1440 1441 1442

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

1443 1444 1445
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
1446
  rescue ProjectWiki::CouldNotCreateWikiError
1447
    errors.add(:base, _('Failed create wiki'))
1448 1449
    false
  end
1450

1451 1452 1453 1454
  def wiki
    @wiki ||= ProjectWiki.new(self, self.owner)
  end

Drew Blessing's avatar
Drew Blessing committed
1455 1456 1457 1458
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end

1459
  def allowed_to_share_with_group?
1460
    !namespace.share_with_group_lock
1461 1462
  end

1463 1464 1465
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1466
    return unless sha
1467

1468
    ci_pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
1469 1470
  end

1471 1472 1473 1474 1475 1476
  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 =
1477
      ci_pipelines.latest_successful_for(default_branch)
1478 1479 1480 1481
  end

  def latest_successful_pipeline_for(ref = nil)
    if ref && ref != default_branch
1482
      ci_pipelines.latest_successful_for(ref)
1483 1484 1485 1486 1487
    else
      latest_successful_pipeline_for_default_branch
    end
  end

1488
  def enable_ci
1489
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1490
  end
Marin Jankovski's avatar
Marin Jankovski committed
1491

1492 1493 1494 1495 1496
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1497
    @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
1498 1499
  end

1500
  def group_runners
1501
    @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
1502 1503
  end

1504
  def all_runners
1505
    Ci::Runner.from_union([runners, group_runners, shared_runners])
1506
  end
1507

1508 1509 1510 1511 1512
  def active_runners
    strong_memoize(:active_runners) do
      all_runners.active
    end
  end
1513

1514 1515
  def any_runners?(&block)
    active_runners.any?(&block)
1516 1517
  end

1518
  def valid_runners_token?(token)
James Lopez's avatar
James Lopez committed
1519
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1520 1521
  end

1522
  # rubocop: disable CodeReuse/ServiceClass
Felipe Artur's avatar
Felipe Artur committed
1523 1524
  def open_issues_count(current_user = nil)
    Projects::OpenIssuesCountService.new(self, current_user).count
1525
  end
1526
  # rubocop: enable CodeReuse/ServiceClass
1527

1528
  # rubocop: disable CodeReuse/ServiceClass
1529 1530
  def open_merge_requests_count
    Projects::OpenMergeRequestsCountService.new(self).count
1531
  end
1532
  # rubocop: enable CodeReuse/ServiceClass
1533

1534
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
Douwe Maan's avatar
Douwe Maan committed
1535
    return true unless forked?
1536

1537
    original_project = fork_source
Douwe Maan's avatar
Douwe Maan committed
1538 1539 1540
    return true unless original_project

    level <= original_project.visibility_level
1541
  end
1542

1543 1544
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1545

1546
    level <= group.visibility_level
Marin Jankovski's avatar
Marin Jankovski committed
1547
  end
1548

1549 1550
  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
1551 1552
  end

1553 1554 1555
  def runners_token
    ensure_runners_token!
  end
1556

1557 1558 1559
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1560

1561
  def pages_group_url
1562
    # The host in URL always needs to be downcased
1563 1564
    Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
      "#{prefix}#{pages_subdomain}."
1565
    end.downcase
1566 1567 1568 1569 1570
  end

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

1572
    # If the project path is the same as host, we serve it as group page
1573
    return url if url == "#{Settings.pages.protocol}://#{url_path}"
1574 1575 1576

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

1578 1579
  def pages_subdomain
    full_path.partition('/').first
1580
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1581 1582

  def pages_path
1583 1584
    # 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
1585 1586 1587 1588 1589 1590
  end

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

1591
  def pages_available?
1592
    Gitlab.config.pages.enabled
1593 1594
  end

1595
  def remove_private_deploy_keys
1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607
    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
1608 1609
  end

1610
  # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
1611
  # rubocop: disable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1612
  def remove_pages
1613 1614 1615
    # Projects with a missing namespace cannot have their pages removed
    return unless namespace

1616 1617
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1618 1619 1620
    # 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
1621
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
1622

1623 1624
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
1625
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1626
  end
1627
  # rubocop: enable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1628

1629 1630 1631 1632
  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.
1633
    repository.raw_repository.write_config(full_path: gl_full_path)
1634
  rescue Gitlab::Git::Repository::NoRepository => e
1635
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
1636
    nil
1637 1638
  end

1639 1640
  def after_import
    repository.after_import
1641
    wiki.repository.after_import
1642 1643 1644 1645 1646 1647 1648

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

1649 1650
    import_state.finish
    import_state.remove_jid
1651
    update_project_counter_caches
1652
    after_create_default_branch
1653
    join_pool_repository
1654
    refresh_markdown_cache!
1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667
  end

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

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

1668
  # rubocop: disable CodeReuse/ServiceClass
1669
  def after_create_default_branch
1670
    Projects::ProtectDefaultBranchService.new(self).execute
1671
  end
1672
  # rubocop: enable CodeReuse/ServiceClass
1673

1674
  # Lazy loading of the `pipeline_status` attribute
1675
  def pipeline_status
1676
    @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
1677 1678
  end

1679 1680
  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)
1681 1682 1683 1684 1685 1686 1687

    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
1688

1689 1690
  def import_export_shared
    @import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
1691 1692
  end

James Lopez's avatar
James Lopez committed
1693
  def export_path
1694
    return unless namespace.present? || hashed_storage?(:repository)
1695

1696
    import_export_shared.archive_path
James Lopez's avatar
James Lopez committed
1697
  end
1698

1699 1700 1701
  def export_status
    if export_in_progress?
      :started
1702 1703
    elsif after_export_in_progress?
      :after_export_action
1704
    elsif export_file_exists?
1705 1706 1707 1708 1709 1710 1711
      :finished
    else
      :none
    end
  end

  def export_in_progress?
1712
    import_export_shared.active_export_count > 0
1713 1714
  end

1715 1716 1717 1718
  def after_export_in_progress?
    import_export_shared.after_export_in_progress?
  end

1719
  def remove_exports
1720
    return unless export_file_exists?
1721

1722
    import_export_upload.remove_export_file!
1723
    import_export_upload.save unless import_export_upload.destroyed?
1724
  end
1725

1726
  def export_file_exists?
1727
    export_file&.file
1728
  end
1729

1730 1731
  def export_file
    import_export_upload&.export_file
1732 1733
  end

1734 1735 1736 1737
  def full_path_slug
    Gitlab::Utils.slugify(full_path.to_s)
  end

1738
  def has_ci?
1739
    repository.gitlab_ci_yml || auto_devops_enabled?
1740 1741
  end

1742
  def predefined_variables
1743 1744
    visibility = Gitlab::VisibilityLevel.string_level(visibility_level)

1745 1746 1747 1748 1749 1750 1751 1752
    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)
1753
      .concat(pages_variables)
1754 1755
      .concat(container_registry_variables)
      .concat(auto_devops_variables)
1756
      .concat(api_variables)
1757 1758
  end

1759 1760 1761 1762 1763 1764 1765
  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

1766 1767
  def api_variables
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1768
      variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url)
1769
    end
1770 1771 1772
  end

  def container_registry_variables
1773
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
1774
      break variables unless Gitlab.config.registry.enabled
1775

1776
      variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
1777

1778
      if container_registry_enabled?
1779
        variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url)
1780
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1781
    end
1782 1783
  end

1784 1785 1786 1787 1788 1789 1790 1791 1792
  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

1793
  def ci_variables_for(ref:, environment: nil)
Lin Jen-Shin's avatar
Lin Jen-Shin committed
1794
    # EE would use the environment
1795 1796 1797 1798 1799 1800
    if protected_for?(ref)
      variables
    else
      variables.unprotected
    end
  end
1801

1802
  def protected_for?(ref)
1803
    raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
1804

1805
    resolved_ref = repository.expand_ref(ref) || ref
1806 1807
    return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)

1808 1809 1810 1811 1812
    ref_name = if resolved_ref == ref
                 Gitlab::Git.ref_name(resolved_ref)
               else
                 ref
               end
1813 1814 1815 1816 1817

    if Gitlab::Git.branch_ref?(resolved_ref)
      ProtectedBranch.protected?(self, ref_name)
    elsif Gitlab::Git.tag_ref?(resolved_ref)
      ProtectedTag.protected?(self, ref_name)
1818
    end
1819
  end
1820

1821
  def deployment_variables(environment: nil)
1822
    deployment_platform(environment: environment)&.predefined_variables(project: self) || []
1823 1824
  end

1825 1826 1827
  def auto_devops_variables
    return [] unless auto_devops_enabled?

1828
    (auto_devops || build_auto_devops)&.predefined_variables
1829 1830
  end

1831
  def append_or_update_attribute(name, value)
1832
    old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
1833 1834 1835 1836 1837 1838

    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
1839 1840 1841

  rescue ActiveRecord::RecordNotSaved => e
    handle_update_attribute_error(e, value)
1842 1843
  end

1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857
  # 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
1858
      update_column(:repository_read_only, false)
1859 1860 1861
    end
  end

1862
  def pushes_since_gc
1863
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
1864 1865 1866
  end

  def increment_pushes_since_gc
1867
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
1868 1869 1870
  end

  def reset_pushes_since_gc
1871
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
1872 1873
  end

Douwe Maan's avatar
Douwe Maan committed
1874
  def route_map_for(commit_sha)
1875 1876
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
Douwe Maan's avatar
Douwe Maan committed
1877
        data = repository.route_map_for(sha)
1878 1879
        next unless data

Douwe Maan's avatar
Douwe Maan committed
1880 1881 1882
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1883 1884 1885 1886 1887 1888 1889
      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
1890
    map = route_map_for(commit_sha)
1891 1892
    return unless map

Douwe Maan's avatar
Douwe Maan committed
1893
    map.public_path_for_source_path(path)
1894 1895
  end

1896 1897 1898 1899
  def parent_changed?
    namespace_id_changed?
  end

1900 1901 1902 1903 1904 1905 1906 1907
  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
1908 1909 1910
  # Overridden on EE module
  def multiple_issue_boards_available?
    false
Felipe Artur's avatar
Felipe Artur committed
1911 1912
  end

1913 1914 1915 1916
  def full_path_was
    File.join(namespace.full_path, previous_changes['path'].first)
  end

1917 1918
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
1919
  # @deprecated cannot remove yet because it has an index with its name in elasticsearch
1920 1921
  alias_method :path_with_namespace, :full_path

1922
  # rubocop: disable CodeReuse/ServiceClass
1923 1924 1925
  def forks_count
    Projects::ForksCountService.new(self).count
  end
1926
  # rubocop: enable CodeReuse/ServiceClass
1927

1928
  def legacy_storage?
1929 1930 1931
    [nil, 0].include?(self.storage_version)
  end

1932 1933 1934 1935
  # 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)
1936
    raise ArgumentError, _("Invalid feature") unless HASHED_STORAGE_FEATURES.include?(feature)
1937 1938

    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
1939 1940
  end

1941 1942 1943 1944
  def renamed?
    persisted? && path_changed?
  end

1945 1946 1947 1948 1949 1950 1951 1952
  def human_merge_method
    if merge_method == :ff
      'Fast-forward'
    else
      merge_method.to_s.humanize
    end
  end

1953 1954 1955
  def merge_method
    if self.merge_requests_ff_only_enabled
      :ff
1956 1957
    elsif self.merge_requests_rebase_enabled
      :rebase_merge
1958 1959 1960 1961 1962 1963
    else
      :merge
    end
  end

  def merge_method=(method)
1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974
    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
1975 1976 1977
  end

  def ff_merge_must_be_possible?
1978
    self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
1979 1980
  end

1981
  def migrate_to_hashed_storage!
1982
    return unless storage_upgradable?
1983

1984
    if git_transfer_in_progress?
1985
      HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
1986
    else
1987
      HashedStorage::ProjectMigrateWorker.perform_async(id)
1988 1989 1990
    end
  end

1991 1992 1993 1994
  def rollback_to_legacy_storage!
    return if legacy_storage?

    if git_transfer_in_progress?
1995
      HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
1996
    else
1997
      HashedStorage::ProjectRollbackWorker.perform_async(id)
1998 1999 2000
    end
  end

2001 2002 2003 2004
  def git_transfer_in_progress?
    repo_reference_count > 0 || wiki_reference_count > 0
  end

2005 2006 2007 2008 2009 2010
  def storage_version=(value)
    super

    @storage = nil if storage_version_changed?
  end

2011 2012
  def reference_counter(type: Gitlab::GlRepository::PROJECT)
    Gitlab::ReferenceCounter.new(type.identifier_for_subject(self))
2013 2014
  end

2015 2016 2017
  def badges
    return project_badges unless group

2018 2019 2020 2021
    Badge.from_union([
      project_badges,
      GroupBadge.where(group: group.self_and_ancestors)
    ])
2022 2023
  end

2024 2025 2026 2027 2028 2029 2030 2031
  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)
2032 2033 2034 2035
    merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
  end

  def any_branch_allows_collaboration?(user)
2036
    fetch_branch_allows_collaboration(user)
2037 2038
  end

2039
  def branch_allows_collaboration?(user, branch_name)
2040
    fetch_branch_allows_collaboration(user, branch_name)
2041 2042
  end

2043 2044 2045 2046 2047
  def external_authorization_classification_label
    super || ::Gitlab::CurrentSettings.current_application_settings
               .external_authorization_service_default_label
  end

2048 2049 2050 2051
  def licensed_features
    []
  end

2052 2053
  def toggle_ci_cd_settings!(settings_attribute)
    ci_cd_settings.toggle!(settings_attribute)
2054 2055
  end

2056
  def gitlab_deploy_token
2057
    @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
2058 2059
  end

2060 2061 2062 2063 2064
  def any_lfs_file_locks?
    lfs_file_locks.any?
  end
  request_cache(:any_lfs_file_locks?) { self.id }

2065 2066 2067 2068
  def auto_cancel_pending_pipelines?
    auto_cancel_pending_pipelines == 'enabled'
  end

2069 2070
  def storage
    @storage ||=
2071
      if hashed_storage?(:repository)
2072 2073 2074 2075 2076
        Storage::HashedProject.new(self)
      else
        Storage::LegacyProject.new(self)
      end
  end
2077

2078 2079 2080 2081
  def storage_upgradable?
    storage_version != LATEST_STORAGE_VERSION
  end

2082 2083 2084 2085
  def snippets_visible?(user = nil)
    Ability.allowed?(user, :read_project_snippet, self)
  end

2086 2087 2088 2089
  def max_attachment_size
    Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
  end

2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112
  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

2113
  def leave_pool_repository
2114
    pool_repository&.mark_obsolete_if_last(repository) && update_column(:pool_repository_id, nil)
2115 2116
  end

2117 2118 2119 2120
  def link_pool_repository
    pool_repository&.link_repository(repository)
  end

2121 2122 2123 2124
  def has_pool_repository?
    pool_repository.present?
  end

2125 2126
  private

2127 2128 2129 2130
  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
2131 2132
  end

2133
  def create_new_pool_repository
2134 2135
    pool = PoolRepository.safe_find_or_create_by!(shard: Shard.by_name(repository_storage), source_project: self)
    update!(pool_repository: pool)
2136

2137
    pool.schedule unless pool.scheduled?
2138

2139 2140 2141 2142 2143 2144 2145 2146 2147
    pool
  end

  def join_pool_repository
    return unless pool_repository

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

2148
  def use_hashed_storage
2149
    if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
2150
      self.storage_version = LATEST_STORAGE_VERSION
2151 2152 2153
    end
  end

2154
  def repo_reference_count
2155
    reference_counter.value
2156 2157 2158
  end

  def wiki_reference_count
2159
    reference_counter(type: Gitlab::GlRepository::WIKI).value
2160 2161
  end

2162 2163 2164
  def check_repository_absence!
    return if skip_disk_validation

2165
    if repository_storage.blank? || repository_with_same_path_already_exists?
2166
      errors.add(:base, _('There is already a repository with that name on disk'))
2167 2168 2169 2170 2171
      throw :abort
    end
  end

  def repository_with_same_path_already_exists?
2172
    gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
2173 2174
  end

2175 2176
  def set_timestamps_for_create
    update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at)
2177 2178
  end

2179
  def cross_namespace_reference?(from)
2180 2181 2182 2183 2184
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
2185 2186 2187
    end
  end

2188
  # Check if a reference is being done cross-project
2189 2190 2191 2192
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
2193 2194
  end

2195
  def pushes_since_gc_redis_shared_state_key
2196 2197 2198
    "projects/#{id}/pushes_since_gc"
  end

2199 2200 2201 2202
  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
2203 2204 2205 2206 2207 2208 2209 2210 2211

  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

2212
    errors.add(:base, _("The project is still being deleted. Please try again later."))
2213 2214 2215 2216 2217
  end

  def pending_delete_twin
    return false unless path

2218
    Project.pending_delete.find_by_full_path(full_path)
2219
  end
2220 2221 2222 2223 2224 2225 2226 2227 2228

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

2229
    ContainerRepository.build_root_repository(self).has_tags?
2230
  end
2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242

  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
2243

2244
  def fetch_branch_allows_collaboration(user, branch_name = nil)
2245 2246
    return false unless user

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

2250 2251 2252 2253 2254 2255
      # 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
2256 2257
    end
  end
2258 2259 2260 2261

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