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

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

28 29 30
  # EE specific modules
  prepend EE::Project

31
  extend Gitlab::ConfigHelper
32

33
  BoardLimitExceeded = Class.new(StandardError)
34

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

47 48 49
  # Valids ports to import from
  VALID_IMPORT_PORTS = [22, 80, 443].freeze

50 51
  cache_markdown_field :description, pipeline: :description

52 53
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
54
                                                       allow_nil: true
55

56
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
57

58
  default_value_for :archived, false
59
  default_value_for :visibility_level, gitlab_config_features.visibility_level
60
  default_value_for :resolve_outdated_diff_discussions, false
61
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
62 63
  default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
64 65 66 67 68
  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
69
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
70
  default_value_for :only_mirror_protected_branches, true
71

72
  add_authentication_token_field :runners_token
73

74
  before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) }
75

76
  before_save :ensure_runners_token
77

78
  after_save :update_project_statistics, if: :namespace_id_changed?
79 80 81

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

82
  after_create :create_project_feature, unless: :project_feature
83 84 85 86 87

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

88
  after_create :set_last_activity_at
89
  after_create :set_last_repository_updated_at
90
  after_update :update_forks_visibility_level
91

92
  before_destroy :remove_private_deploy_keys
93 94 95

  use_fast_destroy :build_trace_chunks

96
  after_destroy -> { run_after_commit { remove_pages } }
97
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
98

James Lopez's avatar
James Lopez committed
99
  after_validation :check_pending_delete
100

101
  # Storage specific hooks
102
  after_initialize :use_hashed_storage
103
  after_create :check_repository_absence!
104 105
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
106

107
  acts_as_taggable
108

109
  attr_accessor :old_path_with_namespace
110
  attr_accessor :template_name
111
  attr_writer :pipeline_status
112
  attr_accessor :skip_disk_validation
113

114 115
  alias_attribute :title, :name

116
  # Relations
117
  belongs_to :creator, class_name: 'User'
118
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
119
  belongs_to :namespace
120 121
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
122

123
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
124
  has_many :boards, before_add: :validate_board_limit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
125

Valery Sizov's avatar
Valery Sizov committed
126
  # Project services
127 128
  has_one :campfire_service
  has_one :drone_ci_service
129
  has_one :gitlab_slack_application_service
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
  has_one :hipchat_service
  has_one :flowdock_service
  has_one :assembla_service
  has_one :asana_service
  has_one :gemnasium_service
  has_one :mattermost_slash_commands_service
  has_one :mattermost_service
  has_one :slack_slash_commands_service
  has_one :slack_service
  has_one :buildkite_service
  has_one :bamboo_service
  has_one :teamcity_service
  has_one :pushover_service
  has_one :jira_service
  has_one :redmine_service
  has_one :custom_issue_tracker_service
  has_one :bugzilla_service
  has_one :gitlab_issue_tracker_service, inverse_of: :project
  has_one :external_wiki_service
153
  has_one :kubernetes_service, inverse_of: :project
154 155 156 157 158
  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
159
  has_one :packagist_service
160

161
  # TODO: replace these relations with the fork network versions
162 163
  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
  has_one  :forked_from_project,  through:   :forked_project_link
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
164

165
  has_many :forked_project_links, foreign_key: "forked_from_project_id"
166
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
167 168 169 170 171 172 173
  # TODO: replace these relations with the fork network versions

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

176 177
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project

178
  # Merge Requests for target project should be removed with it
179
  has_many :merge_requests, foreign_key: 'target_project_id'
180
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
181 182 183 184 185 186 187 188 189 190
  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
191

192
  has_many :project_authorizations
193
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
194
  has_many :project_members, -> { where(requested_at: nil) },
195
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
196

197
  alias_method :members, :project_members
198
  has_many :users, through: :project_members
199

200
  has_many :requesters, -> { where.not(requested_at: nil) },
201
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
202
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
203

204
  has_many :deploy_keys_projects
205
  has_many :deploy_keys, through: :deploy_keys_projects
206
  has_many :users_star_projects
207
  has_many :starrers, through: :users_star_projects, source: :user
208
  has_many :releases
209
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
210
  has_many :lfs_objects, through: :lfs_objects_projects
211
  has_many :lfs_file_locks
212
  has_many :project_group_links
213
  has_many :invited_groups, through: :project_group_links, source: :group
214 215
  has_many :pages_domains
  has_many :todos
216
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
217

218 219
  has_many :internal_ids

220
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
221
  has_one :project_feature, inverse_of: :project
222
  has_one :statistics, class_name: 'ProjectStatistics'
223

Shinya Maeda's avatar
Shinya Maeda committed
224
  has_one :cluster_project, class_name: 'Clusters::Project'
225
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
226
  has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
227

228 229
  has_many :prometheus_metrics

230 231 232
  # 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.
233
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
234

235
  has_many :commit_statuses
236
  has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
237
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
238 239 240 241 242

  # 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.
243
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
244
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
245
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
246
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
247
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
248
  has_many :variables, class_name: 'Ci::Variable'
249 250 251 252
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
253
  has_many :project_deploy_tokens
254
  has_many :deploy_tokens, through: :project_deploy_tokens
255

256
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
257
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
258

259
  has_many :project_badges, class_name: 'ProjectBadge'
260
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
261

262 263
  has_many :remote_mirrors, inverse_of: :project

264
  accepts_nested_attributes_for :variables, allow_destroy: true
265
  accepts_nested_attributes_for :project_feature, update_only: true
266
  accepts_nested_attributes_for :import_data
267
  accepts_nested_attributes_for :auto_devops, update_only: true
268

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

273
  delegate :name, to: :owner, allow_nil: true, prefix: true
274
  delegate :members, to: :team, prefix: true
275
  delegate :add_user, :add_users, to: :team
276
  delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
277
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
278

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
279
  # Validations
280
  validates :creator, presence: true, on: :create
281
  validates :description, length: { maximum: 2000 }, allow_blank: true
282
  validates :ci_config_path,
283
    format: { without: %r{(\.{2}|\A/)},
284
              message: 'cannot include leading slash or directory traversal.' },
285 286
    length: { maximum: 255 },
    allow_blank: true
287 288
  validates :name,
    presence: true,
289
    length: { maximum: 255 },
290
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
291
              message: Gitlab::Regex.project_name_regex_message }
292 293
  validates :path,
    presence: true,
294
    project_path: true,
295
    length: { maximum: 255 }
296

297
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
298
  validates :name, uniqueness: { scope: :namespace_id }
299
  validates :import_url, addressable_url: true, if: :external_import?
300
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
301
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
302
  validate :check_limit, on: :create
303
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
304
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
305
  validate :visibility_level_allowed_as_fork
306
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
307
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
308 309 310
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
311
  validates :variables, variable_duplicates: { scope: :environment_scope }
312

313
  # Scopes
314
  scope :pending_delete, -> { where(pending_delete: true) }
315
  scope :without_deleted, -> { where(pending_delete: false) }
316

317 318 319
  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) }
320

321 322
  # 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") }
323 324
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

325
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
326
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
327
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
328
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
329
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
330
  scope :archived, -> { where(archived: true) }
331
  scope :non_archived, -> { where(archived: false) }
332
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
333
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
334
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
335
  scope :with_statistics, -> { includes(:statistics) }
336
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
337 338 339
  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.
340 341
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
342
  end
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357

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

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

  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
358
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
359
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
360
  scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
361

362 363 364 365 366
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

367
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
368

369 370 371 372 373 374
  chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600

  validates :build_timeout, allow_nil: true,
                            numericality: { greater_than_or_equal_to: 600,
                                            message: 'needs to be at least 10 minutes' }

375 376
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
377 378
  def self.public_or_visible_to_user(user = nil)
    if user
379 380 381
      where('EXISTS (?) OR projects.visibility_level IN (?)',
            user.authorizations_for_projects,
            Gitlab::VisibilityLevel.levels_for_user(user))
382
    else
383
      public_to_user
384 385 386
    end
  end

387 388 389
  # project features may be "disabled", "internal" or "enabled". If "internal",
  # they are only available to team members. This scope returns projects where
  # the feature is either enabled, or internal with permission for the user.
390 391 392 393
  #
  # 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.
394
  def self.with_feature_available_for_user(feature, user)
395
    visible = [nil, ProjectFeature::ENABLED]
396

397 398 399 400
    if user&.admin?
      with_feature_enabled(feature)
    elsif user
      column = ProjectFeature.quoted_access_level_column(feature)
401

402 403
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
404 405
              visible,
              ProjectFeature::PRIVATE,
406
              user.authorizations_for_projects)
407 408 409
    else
      with_feature_access_level(feature, visible)
    end
410
  end
411

412 413
  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) }
414

415
  scope :excluding_project, ->(project) { where.not(id: project) }
416

417 418
  scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
  scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") }
419

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
420
  class << self
421 422 423 424 425 426 427
    # 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.
428
    def search(query)
429
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
430
    end
431

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

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

440
    def sort_by_attribute(method)
441 442
      case method.to_s
      when 'storage_size_desc'
443 444 445
        # 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')
446 447 448 449
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
450 451
      else
        order_by(method)
452 453
      end
    end
454 455

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

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

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

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

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

485 486 487 488 489 490 491 492
  def root_namespace
    if namespace.has_parent?
      namespace.root_ancestor
    else
      namespace
    end
  end

493
  def lfs_enabled?
494
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
495

496
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
497 498
  end

499
  def auto_devops_enabled?
500
    if auto_devops&.enabled.nil?
501
      Gitlab::CurrentSettings.auto_devops_enabled?
502 503
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
504
    end
505 506
  end

507
  def has_auto_devops_implicitly_disabled?
508
    auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
509 510
  end

511 512 513 514
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
515
  def team
516
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
517 518 519
  end

  def repository
520
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
521 522
  end

523 524
  def cleanup
    @repository&.cleanup
525 526 527
    @repository = nil
  end

528 529
  alias_method :reload_repository!, :cleanup

530
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
531
    if Gitlab.config.registry.enabled
532
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
533
    end
534 535
  end

536
  def has_container_registry_tags?
537 538 539
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
540
      has_root_container_repository_tags?
541 542
  end

543 544
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
545 546
  end

547 548 549 550
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

551
  # ref can't be HEAD, can only be branch/tag name or SHA
552
  def latest_successful_builds_for(ref = default_branch)
553
    latest_pipeline = pipelines.latest_successful_for(ref)
554 555

    if latest_pipeline
556
      latest_pipeline.builds.latest.with_artifacts_archive
557 558 559
    else
      builds.none
    end
560 561
  end

562
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
563
    sha = repository.merge_base(first_commit_id, second_commit_id)
564
    commit_by(oid: sha) if sha
565 566
  end

567
  def saved?
568
    id && persisted?
569 570
  end

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

582 583 584 585 586 587 588 589
    log_import_activity(job_id)

    job_id
  end

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

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

597
  def reset_cache_and_import_attrs
598 599 600
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end
601

602
    update(import_error: nil)
603 604 605 606 607
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
608
    import_data&.destroy
609 610
  end

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

616
  def import_url=(value)
617 618
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

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

  def import_url
625
    if import_data && super.present?
626
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
627 628 629
      import_url.full_url
    else
      super
630
    end
631 632
  rescue
    super
633
  end
634

James Lopez's avatar
James Lopez committed
635
  def valid_import_url?
636
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
637 638
  end

639
  def create_or_update_import_data(data: nil, credentials: nil)
640
    return if data.nil? && credentials.nil?
641

James Lopez's avatar
James Lopez committed
642
    project_import_data = import_data || build_import_data
643 644 645 646
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
647

648 649 650 651
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
652
  end
653

654
  def import?
655
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
656 657 658
  end

  def external_import?
659 660 661
    import_url.present?
  end

662
  def imported?
663 664 665 666
    import_finished?
  end

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

Tiago Botelho's avatar
Tiago Botelho committed
670 671 672 673 674 675 676 677
  def import_state_args
    {
      status: self[:import_status],
      jid: self[:import_jid],
      last_error: self[:import_error]
    }
  end

678 679
  def ensure_import_state(force: false)
    return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
Tiago Botelho's avatar
Tiago Botelho committed
680
    return unless import_state.nil?
681

682 683
    if persisted?
      create_import_state(import_state_args)
684

685 686 687 688 689 690
      update_column(:import_status, 'none')
    else
      build_import_state(import_state_args)

      self[:import_status] = 'none'
    end
691 692 693
  end

  def import_schedule
694
    ensure_import_state(force: true)
695

696
    import_state.schedule
697 698 699
  end

  def force_import_start
700
    ensure_import_state(force: true)
701

702
    import_state.force_start
703 704 705
  end

  def import_start
706
    ensure_import_state(force: true)
707

708
    import_state.start
709 710 711
  end

  def import_fail
712
    ensure_import_state(force: true)
713

714
    import_state.fail_op
715 716 717
  end

  def import_finish
718
    ensure_import_state(force: true)
719

720
    import_state.finish
721 722 723
  end

  def import_jid=(new_jid)
724
    ensure_import_state(force: true)
725

726
    import_state.jid = new_jid
727 728 729
  end

  def import_jid
Tiago Botelho's avatar
Tiago Botelho committed
730
    ensure_import_state
731 732 733 734 735

    import_state&.jid
  end

  def import_error=(new_error)
736
    ensure_import_state(force: true)
737

738
    import_state.last_error = new_error
739 740 741
  end

  def import_error
Tiago Botelho's avatar
Tiago Botelho committed
742
    ensure_import_state
743 744 745 746 747

    import_state&.last_error
  end

  def import_status=(new_status)
748
    ensure_import_state(force: true)
749

750
    import_state.status = new_status
751 752 753
  end

  def import_status
Tiago Botelho's avatar
Tiago Botelho committed
754
    ensure_import_state
755

Tiago Botelho's avatar
Tiago Botelho committed
756
    import_state&.status || 'none'
757 758 759 760 761 762
  end

  def no_import?
    import_status == 'none'
  end

763
  def import_started?
764 765
    # import? does SQL work so only run it if it looks like there's an import running
    import_status == 'started' && import?
766 767
  end

768 769 770 771
  def import_scheduled?
    import_status == 'scheduled'
  end

772 773 774 775 776 777
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
778 779
  end

780
  def safe_import_url
781
    Gitlab::UrlSanitizer.new(import_url).masked_url
782 783
  end

784 785 786 787
  def bare_repository_import?
    import_type == 'bare_repository'
  end

788 789 790 791
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
792 793 794 795
  def gitea_import?
    import_type == 'gitea'
  end

796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
  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

827
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
828
    unless creator.can_create_project? || namespace.kind == 'group'
829 830 831
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
832
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
833
      else
Phil Hughes's avatar
Phil Hughes committed
834
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
835
      end
836 837
    end
  rescue
838
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
839 840
  end

841 842 843 844 845 846 847 848 849 850 851 852 853
  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.")
gitlabhq's avatar
gitlabhq committed
854 855
  end

856 857 858 859 860 861 862 863 864 865
  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
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
  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

886
  def to_param
887 888 889 890 891
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
892 893
  end

894
  # `from` argument can be a Namespace or Project.
895 896
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
897
      full_path
898
    elsif cross_project_reference?(from)
899 900 901 902
      path
    end
  end

903 904
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
905
      name_with_namespace
906
    elsif cross_project_reference?(from)
907 908
      name
    end
909 910
  end

911
  def web_url
912
    Gitlab::Routing.url_helpers.project_url(self)
913 914
  end

915 916 917 918 919 920 921
  def readme_url
    readme = repository.readme
    if readme
      Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme.path))
    end
  end

922
  def new_issuable_address(author, address_type)
923
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
924

925 926
    author.ensure_incoming_email_token!

927
    suffix = address_type == 'merge_request' ? '+merge-request' : ''
928
    Gitlab::IncomingEmail.reply_address(
929
      "#{full_path}#{suffix}+#{author.incoming_email_token}")
930 931
  end

932
  def build_commit_note(commit)
933
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
934
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
935

936
  def last_activity
937
    last_event
gitlabhq's avatar
gitlabhq committed
938 939 940
  end

  def last_activity_date
941
    [last_activity_at, last_repository_updated_at, updated_at].compact.max
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
942
  end
943

944 945 946
  def project_id
    self.id
  end
randx's avatar
randx committed
947

948
  def get_issue(issue_id, current_user)
949 950 951 952 953
    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
954
      ExternalIssue.new(issue_id, self)
955 956 957
    end
  end

Robert Speicher's avatar
Robert Speicher committed
958
  def issue_exists?(issue_id)
959
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
960 961
  end

962
  def default_issue_tracker
963
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
964 965 966 967 968 969 970 971 972 973
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

974
  def external_issue_reference_pattern
975
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
976 977
  end

978
  def default_issues_tracker?
979
    !external_issue_tracker
980 981 982
  end

  def external_issue_tracker
983 984 985 986 987 988 989 990 991 992 993 994 995 996
    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
997
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
998 999
  end

1000 1001 1002 1003
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
  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
1017
    update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
1018 1019
  end

1020
  def find_or_initialize_services(exceptions: [])
1021 1022
    services_templates = Service.where(template: true)

1023 1024
    available_services_names = Service.available_services_names - exceptions

1025
    available_services = available_services_names.map do |service_name|
1026
      service = find_service(services, service_name)
1027

1028 1029 1030
      if service
        service
      else
1031 1032 1033 1034
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
1035
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
1036
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
1037
        else
1038
          Service.build_from_template(id, template)
1039 1040
        end
      end
1041
    end
1042 1043 1044 1045 1046 1047 1048 1049

    available_services.reject do |service|
      disabled_services.include?(service.to_param)
    end
  end

  def disabled_services
    []
1050 1051
  end

1052 1053 1054 1055
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

1056 1057
  def create_labels
    Label.templates.each do |label|
1058
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
1059
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
1060 1061 1062
    end
  end

1063 1064
  def find_service(list, name)
    list.find { |service| service.to_param == name }
1065
  end
1066

1067
  def ci_services
1068
    services.where(category: :ci)
1069 1070 1071
  end

  def ci_service
1072
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
1073 1074
  end

1075 1076 1077 1078 1079
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
1080
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
1081 1082
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1083
  def jira_tracker?
1084 1085 1086
    issues_tracker.to_param == 'jira'
  end

1087
  def avatar_in_git
1088
    repository.avatar
1089 1090
  end

1091
  def avatar_url(**args)
1092
    Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
sue445's avatar
sue445 committed
1093 1094
  end

1095 1096 1097
  # For compatibility with old code
  def code
    path
1098
  end
1099

1100
  def items_for(entity)
1101 1102 1103 1104 1105 1106 1107
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
1108

1109
  def send_move_instructions(old_path_with_namespace)
1110 1111
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
1112 1113 1114
    run_after_commit do
      NotificationService.new.project_was_moved(self, old_path_with_namespace)
    end
1115
  end
1116 1117

  def owner
1118 1119
    if group
      group
1120
    else
1121
      namespace.try(:owner)
1122 1123
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1124

1125
  def execute_hooks(data, hooks_scope = :push_hooks)
1126
    run_after_commit_or_now do
1127
      hooks.hooks_for(hooks_scope).each do |hook|
1128 1129
        hook.async_execute(data, hooks_scope.to_s)
      end
1130

1131
      SystemHooksService.new.execute_hooks(data, hooks_scope)
1132
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1133 1134
  end

1135 1136
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
1137 1138 1139 1140
    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
1141 1142 1143 1144
    end
  end

  def valid_repo?
1145
    repository.exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1146
  rescue
1147
    errors.add(:path, 'Invalid repository path')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1148 1149 1150 1151
    false
  end

  def url_to_repo
1152
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1153 1154 1155
  end

  def repo_exists?
1156 1157 1158 1159 1160 1161 1162
    strong_memoize(:repo_exists) do
      begin
        repository.exists?
      rescue
        false
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1163 1164 1165
  end

  def root_ref?(branch)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1166
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1167 1168 1169 1170 1171 1172
  end

  def ssh_url_to_repo
    url_to_repo
  end

1173 1174
  def http_url_to_repo
    "#{web_url}.git"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1175 1176
  end

1177
  def forked?
1178 1179 1180 1181 1182
    return true if fork_network && fork_network.root_project != self

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

1186
  def fork_source
1187 1188
    return nil unless forked?

1189 1190 1191
    forked_from_project || fork_network&.root_project
  end

1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203
  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

1204 1205 1206 1207 1208 1209 1210 1211 1212 1213
  # 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

1214 1215 1216 1217
  def personal?
    !group
  end

1218 1219
  # Expires various caches before a project is renamed.
  def expire_caches_before_rename(old_path)
1220
    # TODO: if we start using UUIDs for cache, we don't need to do this HACK anymore
1221 1222 1223 1224
    repo = Repository.new(old_path, self)
    wiki = Repository.new("#{old_path}.wiki", self)

    if repo.exists?
1225
      repo.before_delete
1226 1227 1228
    end

    if wiki.exists?
1229
      wiki.before_delete
1230 1231 1232
    end
  end

1233
  # Check if repository already exists on disk
1234 1235
  def check_repository_path_availability
    return true if skip_disk_validation
1236
    return false unless repository_storage
1237 1238 1239

    expires_full_path_cache # we need to clear cache to validate renames correctly

1240 1241 1242
    # 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?
1243 1244 1245 1246 1247
      errors.add(:base, 'There is already a repository with that name on disk')
      return false
    end

    true
1248 1249
  rescue GRPC::Internal # if the path is too long
    false
1250 1251
  end

1252 1253 1254 1255
  def create_repository(force: false)
    # Forked import is handled asynchronously
    return if forked? && !force

1256
    if gitlab_shell.create_repository(repository_storage, disk_path)
1257 1258 1259 1260 1261 1262 1263 1264
      repository.after_create
      true
    else
      errors.add(:base, 'Failed to create repository via gitlab-shell')
      false
    end
  end

1265 1266
  def hook_attrs(backward: true)
    attrs = {
1267
      id: id,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1268
      name: name,
1269
      description: description,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
1270
      web_url: web_url,
1271
      avatar_url: avatar_url(only_path: false),
1272 1273
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1274
      namespace: namespace.name,
1275
      visibility_level: visibility_level,
1276
      path_with_namespace: full_path,
1277
      default_branch: default_branch,
1278
      ci_config_path: ci_config_path
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1279
    }
1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291

    # 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
1292 1293
  end

1294
  def project_member(user)
1295 1296 1297 1298 1299
    if project_members.loaded?
      project_members.find { |member| member.user_id == user.id }
    else
      project_members.find_by(user_id: user)
    end
1300
  end
1301 1302 1303 1304

  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1305 1306 1307 1308 1309

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1310

1311
  def visibility_level_field
1312
    :visibility_level
1313
  end
1314 1315 1316 1317 1318 1319 1320 1321

  def archive!
    update_attribute(:archived, true)
  end

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

1323
  def change_head(branch)
1324 1325
    if repository.branch_exists?(branch)
      repository.before_change_head
1326
      repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false)
1327 1328 1329 1330 1331 1332 1333
      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
1334
  end
1335

1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348
  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
1349
  end
1350

1351 1352 1353
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1354

1355
  def ensure_repository
1356
    create_repository(force: true) unless repository_exists?
1357 1358
  end

1359 1360 1361 1362
  def repository_exists?
    !!repository.exists?
  end

1363 1364 1365 1366
  def wiki_repository_exists?
    wiki.repository_exists?
  end

1367
  # update visibility_level of forks
1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378
  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

1379 1380 1381
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
1382
  rescue ProjectWiki::CouldNotCreateWikiError
1383
    errors.add(:base, 'Failed create wiki')
1384 1385
    false
  end
1386

1387
  def wiki
Valery Sizov's avatar
Valery Sizov committed
1388
    @wiki ||= ProjectWiki.new(self, self.owner)
1389 1390
  end

1391 1392 1393
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end
1394

1395 1396 1397
  def allowed_to_share_with_group?
    !namespace.share_with_group_lock
  end
1398

1399 1400 1401
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1402
    return unless sha
1403

1404
    pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
1405 1406
  end

1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423
  def latest_successful_pipeline_for_default_branch
    if defined?(@latest_successful_pipeline_for_default_branch)
      return @latest_successful_pipeline_for_default_branch
    end

    @latest_successful_pipeline_for_default_branch =
      pipelines.latest_successful_for(default_branch)
  end

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

1424
  def enable_ci
1425
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1426
  end
Marin Jankovski's avatar
Marin Jankovski committed
1427

1428 1429 1430 1431 1432
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1433
    @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
1434 1435
  end

1436
  def group_runners
1437
    @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
1438 1439
  end

1440 1441 1442
  def all_runners
    union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners])
    Ci::Runner.from("(#{union.to_sql}) ci_runners")
1443
  end
1444 1445

  def any_runners?(&block)
1446 1447 1448
    @active_runners ||= all_runners.active

    @active_runners.any?(&block)
1449 1450
  end

1451
  def valid_runners_token?(token)
James Lopez's avatar
James Lopez committed
1452
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1453 1454
  end

1455
  def open_issues_count
1456 1457 1458 1459 1460
    Projects::OpenIssuesCountService.new(self).count
  end

  def open_merge_requests_count
    Projects::OpenMergeRequestsCountService.new(self).count
1461
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1462

1463
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
1464
    return true unless forked?
1465

Douwe Maan's avatar
Douwe Maan committed
1466 1467
    # self.forked_from_project will be nil before the project is saved, so
    # we need to go through the relation
1468
    original_project = forked_project_link&.forked_from_project
Douwe Maan's avatar
Douwe Maan committed
1469 1470 1471
    return true unless original_project

    level <= original_project.visibility_level
1472
  end
1473

1474 1475
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1476

1477
    level <= group.visibility_level
Marin Jankovski's avatar
Marin Jankovski committed
1478
  end
1479

1480 1481
  def visibility_level_allowed?(level = self.visibility_level)
    visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
Marin Jankovski's avatar
Marin Jankovski committed
1482
  end
1483

1484 1485 1486
  def runners_token
    ensure_runners_token!
  end
1487

1488 1489 1490
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1491

1492
  def pages_group_url
1493
    # The host in URL always needs to be downcased
1494 1495
    Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
      "#{prefix}#{pages_subdomain}."
1496
    end.downcase
1497 1498 1499 1500 1501
  end

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

1503
    # If the project path is the same as host, we serve it as group page
1504
    return url if url == "#{Settings.pages.protocol}://#{url_path}"
1505 1506 1507

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

1509 1510
  def pages_subdomain
    full_path.partition('/').first
1511
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1512 1513

  def pages_path
1514 1515
    # 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
1516 1517 1518 1519 1520 1521
  end

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

1522 1523 1524 1525
  def pages_available?
    Gitlab.config.pages.enabled && !namespace.subgroup?
  end

1526
  def remove_private_deploy_keys
1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538
    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
1539 1540
  end

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

1546 1547
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1548 1549 1550
    # 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
1551
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
1552

1553 1554
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
1555
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1556
  end
1557

1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584
  def rename_repo
    new_full_path = build_full_path

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

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

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

    expire_caches_before_rename(full_path_was)

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

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

1585
  def after_rename_repo
1586 1587
    write_repository_config

1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599
    path_before_change = previous_changes['path'].first

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

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

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

1600 1601 1602 1603
  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.
1604
    repository.raw_repository.write_config(full_path: gl_full_path)
1605 1606 1607 1608 1609
  rescue Gitlab::Git::Repository::NoRepository => e
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
    nil
  end

1610
  def rename_repo_notify!
1611 1612
    # When we import a project overwriting the original project, there
    # is a move operation. In that case we don't want to send the instructions.
1613
    send_move_instructions(full_path_was) unless import_started?
1614 1615 1616 1617 1618 1619 1620 1621
    expires_full_path_cache

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

    reload_repository!
  end

1622 1623 1624 1625 1626
  def after_import
    repository.after_import
    import_finish
    remove_import_jid
    update_project_counter_caches
1627
    after_create_default_branch
1628
    refresh_markdown_cache!
1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641
  end

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

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

1642 1643 1644 1645 1646 1647
  def after_create_default_branch
    return unless default_branch

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

1648
    if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
1649 1650 1651
      params = {
        name: default_branch,
        push_access_levels_attributes: [{
1652
          access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
1653 1654
        }],
        merge_access_levels_attributes: [{
1655
          access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
1656 1657 1658 1659 1660 1661 1662
        }]
      }

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

1663 1664 1665 1666
  def remove_import_jid
    return unless import_jid

    Gitlab::SidekiqStatus.unset(import_jid)
1667

Tiago Botelho's avatar
Tiago Botelho committed
1668
    import_state.update_column(:jid, nil)
1669 1670
  end

Josh Frye's avatar
Josh Frye committed
1671 1672
  def running_or_pending_build_count(force: false)
    Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
1673 1674 1675
      builds.running_or_pending.count(:all)
    end
  end
1676

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

1682
  def mark_import_as_failed(error_message)
1683 1684 1685
    original_errors = errors.dup
    sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)

1686
    import_fail
1687

Tiago Botelho's avatar
Tiago Botelho committed
1688
    import_state.update_column(:last_error, sanitized_message)
1689 1690 1691 1692
  rescue ActiveRecord::ActiveRecordError => e
    Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
  ensure
    @errors = original_errors
1693
  end
James Lopez's avatar
James Lopez committed
1694

1695 1696
  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)
1697 1698 1699 1700 1701 1702 1703

    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
1704

Sean McGivern's avatar
Sean McGivern committed
1705 1706 1707 1708
  def import_export_shared
    @import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
  end

James Lopez's avatar
James Lopez committed
1709
  def export_path
1710 1711
    return nil unless namespace.present? || hashed_storage?(:repository)

Sean McGivern's avatar
Sean McGivern committed
1712
    import_export_shared.archive_path
James Lopez's avatar
James Lopez committed
1713
  end
1714 1715 1716 1717 1718

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

Sean McGivern's avatar
Sean McGivern committed
1719 1720 1721
  def export_status
    if export_in_progress?
      :started
1722 1723
    elsif after_export_in_progress?
      :after_export_action
Sean McGivern's avatar
Sean McGivern committed
1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734
    elsif export_project_path
      :finished
    else
      :none
    end
  end

  def export_in_progress?
    import_export_shared.active_export_count > 0
  end

1735 1736 1737 1738
  def after_export_in_progress?
    import_export_shared.after_export_in_progress?
  end

1739
  def remove_exports
1740 1741 1742
    return nil unless export_path.present?

    FileUtils.rm_rf(export_path)
1743
  end
1744

1745 1746 1747 1748 1749 1750
  def remove_exported_project_file
    return unless export_project_path.present?

    FileUtils.rm_f(export_project_path)
  end

1751 1752 1753 1754
  def full_path_slug
    Gitlab::Utils.slugify(full_path.to_s)
  end

1755
  def has_ci?
1756
    repository.gitlab_ci_yml || auto_devops_enabled?
1757 1758
  end

1759
  def predefined_variables
1760 1761
    visibility = Gitlab::VisibilityLevel.string_level(visibility_level)

1762 1763 1764 1765 1766 1767 1768 1769 1770 1771
    Gitlab::Ci::Variables::Collection.new
      .append(key: 'CI_PROJECT_ID', value: id.to_s)
      .append(key: 'CI_PROJECT_NAME', value: path)
      .append(key: 'CI_PROJECT_PATH', value: full_path)
      .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
      .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
      .append(key: 'CI_PROJECT_URL', value: web_url)
      .append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
      .concat(container_registry_variables)
      .concat(auto_devops_variables)
1772 1773 1774
  end

  def container_registry_variables
1775
    Gitlab::Ci::Variables::Collection.new.tap do |variables|
Jacopo's avatar
Jacopo committed
1776
      break variables unless Gitlab.config.registry.enabled
1777

1778
      variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
1779

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

1786 1787
  def secret_variables_for(ref:, environment: nil)
    # EE would use the environment
1788 1789 1790 1791
    if protected_for?(ref)
      variables
    else
      variables.unprotected
1792 1793
    end
  end
1794

1795
  def protected_for?(ref)
1796 1797 1798
    if repository.branch_exists?(ref)
      ProtectedBranch.protected?(self, ref)
    elsif repository.tag_exists?(ref)
1799
      ProtectedTag.protected?(self, ref)
1800
    end
1801
  end
1802

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

1807 1808 1809
  def auto_devops_variables
    return [] unless auto_devops_enabled?

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

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

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

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

1826
  def pushes_since_gc
1827
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
1828 1829 1830
  end

  def increment_pushes_since_gc
1831
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
1832 1833 1834
  end

  def reset_pushes_since_gc
1835
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
1836 1837
  end

Douwe Maan's avatar
Douwe Maan committed
1838
  def route_map_for(commit_sha)
1839 1840
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
Douwe Maan's avatar
Douwe Maan committed
1841
        data = repository.route_map_for(sha)
1842
        next unless data
1843

Douwe Maan's avatar
Douwe Maan committed
1844 1845 1846
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1847
      end
1848
    end
1849 1850

    @route_maps_by_commit[commit_sha]
1851 1852
  end

1853
  def public_path_for_source_path(path, commit_sha)
Douwe Maan's avatar
Douwe Maan committed
1854
    map = route_map_for(commit_sha)
1855 1856
    return unless map

Douwe Maan's avatar
Douwe Maan committed
1857
    map.public_path_for_source_path(path)
1858 1859
  end

1860 1861 1862 1863
  def parent_changed?
    namespace_id_changed?
  end

1864 1865 1866 1867 1868 1869 1870 1871
  def default_merge_request_target
    if forked_from_project&.merge_requests_enabled?
      forked_from_project
    else
      self
    end
  end

1872 1873 1874
  # Overridden on EE module
  def multiple_issue_boards_available?
    false
1875
  end
1876

1877 1878 1879 1880
  def full_path_was
    File.join(namespace.full_path, previous_changes['path'].first)
  end

1881 1882
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
1883
  # @deprecated cannot remove yet because it has an index with its name in elasticsearch
1884 1885
  alias_method :path_with_namespace, :full_path

1886 1887 1888 1889
  def forks_count
    Projects::ForksCountService.new(self).count
  end

1890
  def legacy_storage?
1891 1892 1893
    [nil, 0].include?(self.storage_version)
  end

1894 1895 1896 1897
  # 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)
1898 1899 1900
    raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)

    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
1901 1902
  end

1903 1904 1905 1906
  def renamed?
    persisted? && path_changed?
  end

1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934
  def merge_method
    if self.merge_requests_ff_only_enabled
      :ff
    elsif self.merge_requests_rebase_enabled
      :rebase_merge
    else
      :merge
    end
  end

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

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

1935
  def migrate_to_hashed_storage!
1936
    return if hashed_storage?(:repository)
1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956

    update!(repository_read_only: true)

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

  def storage_version=(value)
    super

    @storage = nil if storage_version_changed?
  end

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

1957 1958 1959 1960
  def reference_counter(wiki: false)
    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
  end

1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971
  # Refreshes the expiration time of the associated import job ID.
  #
  # This method can be used by asynchronous importers to refresh the status,
  # preventing the StuckImportJobsWorker from marking the import as failed.
  def refresh_import_jid_expiration
    return unless import_jid

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

1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982
  def badges
    return project_badges unless group

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

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

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

1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009
  def merge_requests_allowing_push_to_user(user)
    return MergeRequest.none unless user

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

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

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

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

    memoized_results[cache_key]
  end

2010 2011 2012 2013
  def licensed_features
    []
  end

2014 2015
  def toggle_ci_cd_settings!(settings_attribute)
    ci_cd_settings.toggle!(settings_attribute)
2016 2017
  end

2018
  def gitlab_deploy_token
2019
    @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
2020 2021
  end

2022 2023
  private

2024 2025
  def storage
    @storage ||=
2026
      if hashed_storage?(:repository)
2027 2028 2029 2030 2031
        Storage::HashedProject.new(self)
      else
        Storage::LegacyProject.new(self)
      end
  end
2032

2033
  def use_hashed_storage
2034
    if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
2035
      self.storage_version = LATEST_STORAGE_VERSION
2036 2037 2038
    end
  end

2039
  def repo_reference_count
2040
    reference_counter.value
2041 2042 2043
  end

  def wiki_reference_count
2044
    reference_counter(wiki: true).value
2045 2046
  end

2047 2048 2049
  def check_repository_absence!
    return if skip_disk_validation

2050
    if repository_storage.blank? || repository_with_same_path_already_exists?
2051 2052 2053 2054 2055 2056
      errors.add(:base, 'There is already a repository with that name on disk')
      throw :abort
    end
  end

  def repository_with_same_path_already_exists?
2057
    gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
2058 2059
  end

2060 2061 2062 2063 2064 2065 2066 2067 2068
  # set last_activity_at to the same as created_at
  def set_last_activity_at
    update_column(:last_activity_at, self.created_at)
  end

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

2069
  def cross_namespace_reference?(from)
2070 2071 2072 2073 2074
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
2075 2076 2077
    end
  end

2078
  # Check if a reference is being done cross-project
2079 2080 2081 2082
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
2083 2084
  end

2085
  def pushes_since_gc_redis_shared_state_key
2086 2087 2088
    "projects/#{id}/pushes_since_gc"
  end

2089 2090 2091 2092 2093 2094 2095
  # 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.
2096
  def validate_board_limit(board)
2097
    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
2098
  end
2099 2100 2101 2102 2103

  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
James Lopez's avatar
James Lopez committed
2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118

  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

2119
    Project.pending_delete.find_by_full_path(full_path)
James Lopez's avatar
James Lopez committed
2120
  end
2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131

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

    ContainerRepository.build_root_repository(self).has_tags?
  end
2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143

  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
2144 2145 2146

  def fetch_branch_allows_maintainer_push?(user, branch_name)
    check_access = -> do
2147 2148
      next false if empty_repo?

2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162
      merge_request = source_of_merge_requests.opened
                        .where(allow_maintainer_to_push: true)
                        .find_by(source_branch: branch_name)
      merge_request&.can_be_merged_by?(user)
    end

    if RequestStore.active?
      RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do
        check_access.call
      end
    else
      check_access.call
    end
  end
gitlabhq's avatar
gitlabhq committed
2163
end