project.rb 55.5 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 Gitlab::CurrentSettings
8
  include AccessRequestable
9
  include Avatarable
10
  include CacheMarkdownField
11 12
  include Referable
  include Sortable
13
  include AfterCommitQueue
14
  include CaseSensitivity
15
  include TokenAuthenticatable
James Lopez's avatar
James Lopez committed
16
  include ValidAttribute
17
  include ProjectFeaturesCompatibility
18
  include SelectForProjectAuthorization
19
  include Routable
20
  include GroupDescendant
21
  include Gitlab::SQL::Pattern
22
  include DeploymentPlatform
23
  include ::Gitlab::Utils::StrongMemoize
24

25 26 27
  # EE specific modules
  prepend EE::Project

28
  extend Gitlab::ConfigHelper
29
  extend Gitlab::CurrentSettings
30

31
  BoardLimitExceeded = Class.new(StandardError)
32

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

45 46
  cache_markdown_field :description, pipeline: :description

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

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

53
  default_value_for :archived, false
54
  default_value_for :visibility_level, gitlab_config_features.visibility_level
55
  default_value_for :resolve_outdated_diff_discussions, false
56
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
57
  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
58
  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
59 60 61 62 63
  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
64
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
65
  default_value_for :only_mirror_protected_branches, true
66

67 68
  add_authentication_token_field :runners_token
  before_save :ensure_runners_token
69

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

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

James Lopez's avatar
James Lopez committed
79
  after_validation :check_pending_delete
80

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

87
  acts_as_taggable
88

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

94 95
  alias_attribute :title, :name

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

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

Valery Sizov's avatar
Valery Sizov committed
106
  # Project services
107 108
  has_one :campfire_service
  has_one :drone_ci_service
109
  has_one :gitlab_slack_application_service
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
  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
133
  has_one :kubernetes_service, inverse_of: :project
134 135 136 137 138
  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
139
  has_one :packagist_service
140

141
  # TODO: replace these relations with the fork network versions
142
  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
143
  has_one  :forked_from_project,  through:   :forked_project_link
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
144

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

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

156
  # Merge Requests for target project should be removed with it
157 158 159 160 161 162 163 164 165 166 167
  has_many :merge_requests, foreign_key: 'target_project_id'
  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
168

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

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

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

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

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

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

201 202 203
  # 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.
204
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
205

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

  # 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.
213
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
214
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
215
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
216
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
217
  has_many :variables, class_name: 'Ci::Variable'
218 219 220 221
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
222

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

225
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
226
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
227

228
  accepts_nested_attributes_for :variables, allow_destroy: true
229
  accepts_nested_attributes_for :project_feature, update_only: true
230
  accepts_nested_attributes_for :import_data
231
  accepts_nested_attributes_for :auto_devops, update_only: true
232

233
  delegate :name, to: :owner, allow_nil: true, prefix: true
234
  delegate :members, to: :team, prefix: true
235
  delegate :add_user, :add_users, to: :team
236
  delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
237

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

257
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
258
  validates :name, uniqueness: { scope: :namespace_id }
259
  validates :import_url, addressable_url: true, if: :external_import?
260
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
261
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
262
  validate :check_limit, on: :create
263
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
264
  validate :avatar_type,
265
    if: ->(project) { project.avatar.present? && project.avatar_changed? }
266
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
267
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
268
  validate :visibility_level_allowed_as_fork
269
  validate :check_wiki_path_conflict
270 271 272
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
273

Douwe Maan's avatar
Douwe Maan committed
274
  mount_uploader :avatar, AvatarUploader
275
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
276

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

281 282 283
  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) }
284

285
  scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
286 287
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

288
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
289
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
290
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
291
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
292
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
293
  scope :archived, -> { where(archived: true) }
294
  scope :non_archived, -> { where(archived: false) }
295
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
296
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
297
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
298
  scope :with_statistics, -> { includes(:statistics) }
299
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
300 301 302
  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.
303 304
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
305
  end
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320

  # "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) }
321
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
322
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
323

324
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
325

326 327 328 329
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
  def self.public_or_visible_to_user(user = nil)
    if user
330 331 332 333
      authorized = user
        .project_authorizations
        .select(1)
        .where('project_authorizations.project_id = projects.id')
334 335 336 337 338 339 340 341 342

      levels = Gitlab::VisibilityLevel.levels_for_user(user)

      where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels)
    else
      public_to_user
    end
  end

343 344 345
  # 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.
346 347 348 349
  #
  # 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.
350
  def self.with_feature_available_for_user(feature, user)
351
    visible = [nil, ProjectFeature::ENABLED]
352

353 354 355 356
    if user&.admin?
      with_feature_enabled(feature)
    elsif user
      column = ProjectFeature.quoted_access_level_column(feature)
357

358 359
      authorized = user.project_authorizations.select(1)
        .where('project_authorizations.project_id = projects.id')
360

361 362
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
363 364 365 366 367 368
              visible,
              ProjectFeature::PRIVATE,
              authorized)
    else
      with_feature_access_level(feature, visible)
    end
369
  end
370

371 372
  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) }
373

374
  scope :excluding_project, ->(project) { where.not(id: project) }
375
  scope :import_started, -> { where(import_status: 'started') }
376

377
  state_machine :import_status, initial: :none do
378 379 380 381 382 383 384 385
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

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

386
    event :import_start do
387
      transition scheduled: :started
388 389 390
    end

    event :import_finish do
391
      transition started: :finished
392 393 394
    end

    event :import_fail do
395
      transition [:scheduled, :started] => :failed
396 397
    end

398
    state :scheduled
399 400
    state :started
    state :finished
401 402
    state :failed

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

410 411
    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs
412 413 414

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        project.run_after_commit do
415
          Projects::AfterImportService.new(project).execute
416 417
        end
      end
418
    end
419 420
  end

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

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

437 438 439
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
440 441

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

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

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

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

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

479 480 481 482 483 484 485
  # 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

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

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

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

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

508 509 510 511
  def has_auto_devops_implicitly_disabled?
    auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
  end

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

516
  def repository_storage_path
517
    Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
518 519
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
520
  def team
521
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
522 523 524
  end

  def repository
525
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
526 527
  end

528 529 530 531
  def reload_repository!
    @repository = nil
  end

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

538
  def has_container_registry_tags?
539 540 541
    return @images if defined?(@images)

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

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

549 550 551 552
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

553
  # ref can't be HEAD, can only be branch/tag name or SHA
554
  def latest_successful_builds_for(ref = default_branch)
555
    latest_pipeline = pipelines.latest_successful_for(ref)
556 557 558 559 560 561

    if latest_pipeline
      latest_pipeline.builds.latest.with_artifacts
    else
      builds.none
    end
562 563
  end

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

569
  def saved?
570
    id && persisted?
571 572
  end

573
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
574 575
    job_id =
      if forked?
576 577
        RepositoryForkWorker.perform_async(id,
                                           forked_from_project.repository_storage_path,
578
                                           forked_from_project.disk_path)
Douwe Maan's avatar
Douwe Maan committed
579 580 581
      else
        RepositoryImportWorker.perform_async(self.id)
      end
582

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

    job_id
  end

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

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

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

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

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

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

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

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

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

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

640
  def create_or_update_import_data(data: nil, credentials: nil)
641
    return unless import_url.present? && valid_import_url?
642

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

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

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

659 660 661 662
  def no_import?
    import_status == 'none'
  end

663
  def external_import?
664 665 666
    import_url.present?
  end

667
  def imported?
668 669 670 671
    import_finished?
  end

  def import_in_progress?
672 673 674 675
    import_started? || import_scheduled?
  end

  def import_started?
676 677
    # import? does SQL work so only run it if it looks like there's an import running
    import_status == 'started' && import?
678 679
  end

680 681 682 683
  def import_scheduled?
    import_status == 'scheduled'
  end

684 685 686 687 688 689
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
690 691
  end

692
  def safe_import_url
693
    Gitlab::UrlSanitizer.new(import_url).masked_url
694 695
  end

696 697 698 699
  def bare_repository_import?
    import_type == 'bare_repository'
  end

700 701 702 703
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
704 705 706 707
  def gitea_import?
    import_type == 'gitea'
  end

708
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
709
    unless creator.can_create_project? || namespace.kind == 'group'
710 711 712
      projects_limit = creator.projects_limit

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

722 723 724 725 726 727 728 729 730 731 732 733 734
  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
735 736
  end

737 738 739 740 741 742 743 744 745 746
  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

747
  def to_param
748 749 750 751 752
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
753 754
  end

755
  # `from` argument can be a Namespace or Project.
756 757
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
758
      full_path
759
    elsif cross_project_reference?(from)
760 761 762 763
      path
    end
  end

764 765
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
766
      name_with_namespace
767
    elsif cross_project_reference?(from)
768 769
      name
    end
770 771
  end

772
  def web_url
773
    Gitlab::Routing.url_helpers.project_url(self)
774 775
  end

776
  def new_issuable_address(author, address_type)
777
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
778

779 780
    author.ensure_incoming_email_token!

781
    suffix = address_type == 'merge_request' ? '+merge-request' : ''
782
    Gitlab::IncomingEmail.reply_address(
783
      "#{full_path}#{suffix}+#{author.incoming_email_token}")
784 785
  end

786
  def build_commit_note(commit)
787
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
788
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
789

790
  def last_activity
791
    last_event
gitlabhq's avatar
gitlabhq committed
792 793 794
  end

  def last_activity_date
795
    last_repository_updated_at || last_activity_at || updated_at
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
796
  end
797

798 799 800
  def project_id
    self.id
  end
randx's avatar
randx committed
801

802
  def get_issue(issue_id, current_user)
803 804 805 806 807
    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
808
      ExternalIssue.new(issue_id, self)
809 810 811
    end
  end

Robert Speicher's avatar
Robert Speicher committed
812
  def issue_exists?(issue_id)
813
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
814 815
  end

816
  def default_issue_tracker
817
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
818 819 820 821 822 823 824 825 826 827
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

828
  def external_issue_reference_pattern
829
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
830 831
  end

832
  def default_issues_tracker?
833
    !external_issue_tracker
834 835 836
  end

  def external_issue_tracker
837 838 839 840 841 842 843 844 845 846 847 848 849 850
    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
851
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
852 853
  end

854 855 856 857
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

858 859 860 861 862 863 864 865 866 867 868 869 870
  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
871
    update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
872 873
  end

874
  def find_or_initialize_services(exceptions: [])
875 876
    services_templates = Service.where(template: true)

877 878 879
    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
880
      service = find_service(services, service_name)
881

882 883 884
      if service
        service
      else
885 886 887 888
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
889
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
890
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
891
        else
892
          Service.build_from_template(id, template)
893 894
        end
      end
895 896 897
    end
  end

898 899 900 901
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

902 903
  def create_labels
    Label.templates.each do |label|
904
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
905
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
906 907 908
    end
  end

909 910
  def find_service(list, name)
    list.find { |service| service.to_param == name }
911
  end
912

913
  def ci_services
914
    services.where(category: :ci)
915 916 917
  end

  def ci_service
918
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
919 920
  end

921 922 923 924 925
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
926
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
927 928
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
929
  def jira_tracker?
930 931 932
    issues_tracker.to_param == 'jira'
  end

933
  def avatar_type
934 935
    unless self.avatar.image?
      self.errors.add :avatar, 'only images allowed'
936
    end
Marin Jankovski's avatar
Marin Jankovski committed
937 938
  end

939
  def avatar_in_git
940
    repository.avatar
941 942
  end

943 944 945
  def avatar_url(**args)
    # We use avatar_path instead of overriding avatar_url because of carrierwave.
    # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
946
    avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
sue445's avatar
sue445 committed
947 948
  end

949 950 951
  # For compatibility with old code
  def code
    path
952
  end
953

954
  def items_for(entity)
955 956 957 958 959 960 961
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
962

963
  def send_move_instructions(old_path_with_namespace)
964 965
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
966 967 968
    run_after_commit do
      NotificationService.new.project_was_moved(self, old_path_with_namespace)
    end
969
  end
970 971

  def owner
972 973
    if group
      group
974
    else
975
      namespace.try(:owner)
976 977
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
978

979
  def execute_hooks(data, hooks_scope = :push_hooks)
980
    run_after_commit_or_now do
981
      hooks.hooks_for(hooks_scope).each do |hook|
982 983
        hook.async_execute(data, hooks_scope.to_s)
      end
984
    end
985 986

    SystemHooksService.new.execute_hooks(data, hooks_scope)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
987 988
  end

989 990
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
991 992 993 994
    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
995 996 997 998
    end
  end

  def valid_repo?
999
    repository.exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1000
  rescue
1001
    errors.add(:path, 'Invalid repository path')
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1002 1003 1004 1005
    false
  end

  def url_to_repo
1006
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1007 1008 1009
  end

  def repo_exists?
1010 1011 1012 1013 1014 1015 1016
    strong_memoize(:repo_exists) do
      begin
        repository.exists?
      rescue
        false
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1017 1018 1019
  end

  def root_ref?(branch)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1020
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1021 1022 1023 1024 1025 1026
  end

  def ssh_url_to_repo
    url_to_repo
  end

1027 1028
  def http_url_to_repo
    "#{web_url}.git"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1029 1030
  end

1031
  def user_can_push_to_empty_repo?(user)
1032
    !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
1033 1034
  end

1035
  def forked?
1036 1037 1038 1039 1040
    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
1041 1042
    !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1043

1044 1045 1046 1047
  def fork_source
    forked_from_project || fork_network&.root_project
  end

1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059
  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

1060 1061 1062 1063
  def personal?
    !group
  end

1064 1065
  # Expires various caches before a project is renamed.
  def expire_caches_before_rename(old_path)
1066
    # TODO: if we start using UUIDs for cache, we don't need to do this HACK anymore
1067 1068 1069 1070
    repo = Repository.new(old_path, self)
    wiki = Repository.new("#{old_path}.wiki", self)

    if repo.exists?
1071
      repo.before_delete
1072 1073 1074
    end

    if wiki.exists?
1075
      wiki.before_delete
1076 1077 1078
    end
  end

1079
  # Check if repository already exists on disk
1080 1081
  def check_repository_path_availability
    return true if skip_disk_validation
1082 1083 1084 1085
    return false unless repository_storage_path

    expires_full_path_cache # we need to clear cache to validate renames correctly

1086 1087 1088
    # 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?
1089 1090 1091 1092 1093
      errors.add(:base, 'There is already a repository with that name on disk')
      return false
    end

    true
1094 1095
  rescue GRPC::Internal # if the path is too long
    false
1096 1097
  end

1098 1099 1100 1101
  def create_repository(force: false)
    # Forked import is handled asynchronously
    return if forked? && !force

1102
    if gitlab_shell.add_repository(repository_storage, disk_path)
1103 1104 1105 1106 1107 1108 1109 1110
      repository.after_create
      true
    else
      errors.add(:base, 'Failed to create repository via gitlab-shell')
      false
    end
  end

1111 1112
  def hook_attrs(backward: true)
    attrs = {
1113
      id: id,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1114
      name: name,
1115
      description: description,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
1116
      web_url: web_url,
1117
      avatar_url: avatar_url(only_path: false),
1118 1119
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1120
      namespace: namespace.name,
1121
      visibility_level: visibility_level,
1122
      path_with_namespace: full_path,
1123
      default_branch: default_branch,
1124
      ci_config_path: ci_config_path
Kirill Zaitsev's avatar
Kirill Zaitsev committed
1125
    }
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137

    # 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
1138 1139
  end

1140
  def project_member(user)
1141 1142 1143 1144 1145
    if project_members.loaded?
      project_members.find { |member| member.user_id == user.id }
    else
      project_members.find_by(user_id: user)
    end
1146
  end
1147 1148 1149 1150

  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1151 1152 1153 1154 1155

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1156

1157
  def visibility_level_field
1158
    :visibility_level
1159
  end
1160 1161 1162 1163 1164 1165 1166 1167

  def archive!
    update_attribute(:archived, true)
  end

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

1169
  def change_head(branch)
1170 1171
    if repository.branch_exists?(branch)
      repository.before_change_head
1172
      repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}", shell: false)
1173 1174 1175 1176 1177 1178 1179
      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
1180
  end
1181

1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
  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
1195
  end
1196

1197 1198 1199
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1200

1201
  def ensure_repository
1202
    create_repository(force: true) unless repository_exists?
1203 1204
  end

1205 1206 1207 1208
  def repository_exists?
    !!repository.exists?
  end

1209 1210 1211 1212
  def wiki_repository_exists?
    wiki.repository_exists?
  end

1213
  # update visibility_level of forks
1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
  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

1225 1226 1227
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
1228
  rescue ProjectWiki::CouldNotCreateWikiError
1229
    errors.add(:base, 'Failed create wiki')
1230 1231
    false
  end
1232

1233
  def wiki
Valery Sizov's avatar
Valery Sizov committed
1234
    @wiki ||= ProjectWiki.new(self, self.owner)
1235 1236
  end

1237 1238 1239
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end
1240

1241 1242 1243
  def allowed_to_share_with_group?
    !namespace.share_with_group_lock
  end
1244

1245 1246 1247
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1248
    return unless sha
1249

1250
    pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
1251 1252
  end

1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269
  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

1270
  def enable_ci
1271
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1272
  end
Marin Jankovski's avatar
Marin Jankovski committed
1273

1274 1275 1276 1277 1278
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1279
    @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
1280 1281
  end

1282 1283
  def active_shared_runners
    @active_shared_runners ||= shared_runners.active
1284
  end
1285 1286

  def any_runners?(&block)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1287
    active_runners.any?(&block) || active_shared_runners.any?(&block)
1288 1289
  end

1290
  def valid_runners_token?(token)
James Lopez's avatar
James Lopez committed
1291
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1292 1293
  end

1294 1295 1296 1297 1298 1299 1300
  def build_timeout_in_minutes
    build_timeout / 60
  end

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

1302
  def open_issues_count
1303 1304 1305 1306 1307
    Projects::OpenIssuesCountService.new(self).count
  end

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

1310
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
1311
    return true unless forked?
1312

Douwe Maan's avatar
Douwe Maan committed
1313 1314
    # self.forked_from_project will be nil before the project is saved, so
    # we need to go through the relation
1315
    original_project = forked_project_link&.forked_from_project
Douwe Maan's avatar
Douwe Maan committed
1316 1317 1318
    return true unless original_project

    level <= original_project.visibility_level
1319
  end
1320

1321 1322
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1323

1324
    level <= group.visibility_level
Marin Jankovski's avatar
Marin Jankovski committed
1325
  end
1326

1327 1328
  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
1329
  end
1330

1331 1332 1333
  def runners_token
    ensure_runners_token!
  end
1334

1335 1336 1337
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1338

1339
  def pages_url
1340 1341
    subdomain, _, url_path = full_path.partition('/')

1342 1343
    # The hostname always needs to be in downcased
    # All web servers convert hostname to lowercase
1344
    host = "#{subdomain}.#{Settings.pages.host}".downcase
1345 1346

    # The host in URL always needs to be downcased
1347
    url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
1348
      "#{prefix}#{subdomain}."
1349
    end.downcase
1350

1351
    # If the project path is the same as host, we serve it as group page
1352 1353 1354 1355
    return url if host == url_path

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

1357 1358
  def pages_subdomain
    full_path.partition('/').first
1359
  end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1360 1361

  def pages_path
1362 1363
    # 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
1364 1365 1366 1367 1368 1369
  end

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

1370 1371 1372 1373
  def pages_available?
    Gitlab.config.pages.enabled && !namespace.subgroup?
  end

1374
  def remove_private_deploy_keys
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386
    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
1387 1388
  end

1389
  # 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
1390
  def remove_pages
1391 1392 1393
    # Projects with a missing namespace cannot have their pages removed
    return unless namespace

1394 1395
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1396 1397 1398
    # 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
1399
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
1400

1401 1402
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
1403
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1404
  end
1405

1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432
  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

1433
  def after_rename_repo
1434 1435
    write_repository_config

1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447
    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

1448 1449 1450 1451
  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.
1452
    repository.raw_repository.write_config(full_path: gl_full_path)
1453 1454 1455 1456 1457
  rescue Gitlab::Git::Repository::NoRepository => e
    Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
    nil
  end

1458 1459 1460 1461 1462 1463 1464 1465 1466 1467
  def rename_repo_notify!
    send_move_instructions(full_path_was)
    expires_full_path_cache

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

    reload_repository!
  end

1468 1469 1470 1471 1472
  def after_import
    repository.after_import
    import_finish
    remove_import_jid
    update_project_counter_caches
1473
    after_create_default_branch
1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486
  end

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

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

1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507
  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)

    if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch)
      params = {
        name: default_branch,
        push_access_levels_attributes: [{
          access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
        }],
        merge_access_levels_attributes: [{
          access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
        }]
      }

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

1508 1509 1510 1511 1512 1513 1514
  def remove_import_jid
    return unless import_jid

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

Josh Frye's avatar
Josh Frye committed
1515 1516
  def running_or_pending_build_count(force: false)
    Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
1517 1518 1519
      builds.running_or_pending.count(:all)
    end
  end
1520

1521
  # Lazy loading of the `pipeline_status` attribute
1522
  def pipeline_status
1523
    @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
1524 1525
  end

1526
  def mark_import_as_failed(error_message)
1527 1528 1529
    original_errors = errors.dup
    sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)

1530
    import_fail
1531 1532 1533 1534 1535
    update_column(:import_error, sanitized_message)
  rescue ActiveRecord::ActiveRecordError => e
    Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
  ensure
    @errors = original_errors
1536
  end
James Lopez's avatar
James Lopez committed
1537

1538 1539
  def add_export_job(current_user:)
    job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
1540 1541 1542 1543 1544 1545 1546

    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
1547 1548

  def export_path
1549
    File.join(Gitlab::ImportExport.storage_path, disk_path)
James Lopez's avatar
James Lopez committed
1550
  end
1551 1552 1553 1554 1555 1556 1557 1558 1559

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

  def remove_exports
    _, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
    status.zero?
  end
1560

1561 1562 1563 1564
  def full_path_slug
    Gitlab::Utils.slugify(full_path.to_s)
  end

1565
  def has_ci?
1566
    repository.gitlab_ci_yml || auto_devops_enabled?
1567 1568
  end

1569 1570 1571 1572
  def predefined_variables
    [
      { key: 'CI_PROJECT_ID', value: id.to_s, public: true },
      { key: 'CI_PROJECT_NAME', value: path, public: true },
1573
      { key: 'CI_PROJECT_PATH', value: full_path, public: true },
1574
      { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
1575
      { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
1576 1577
      { key: 'CI_PROJECT_URL', value: web_url, public: true },
      { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
1578 1579 1580 1581 1582 1583 1584 1585 1586 1587
    ]
  end

  def container_registry_variables
    return [] unless Gitlab.config.registry.enabled

    variables = [
      { key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port, public: true }
    ]

Kamil Trzcinski's avatar
Kamil Trzcinski committed
1588
    if container_registry_enabled?
1589
      variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1590 1591
    end

1592 1593 1594
    variables
  end

1595 1596
  def secret_variables_for(ref:, environment: nil)
    # EE would use the environment
1597 1598 1599 1600
    if protected_for?(ref)
      variables
    else
      variables.unprotected
1601 1602
    end
  end
1603

1604 1605 1606
  def protected_for?(ref)
    ProtectedBranch.protected?(self, ref) ||
      ProtectedTag.protected?(self, ref)
1607
  end
1608

1609
  def deployment_variables(environment: nil)
1610
    deployment_platform(environment: environment)&.predefined_variables || []
1611 1612
  end

1613 1614 1615 1616 1617 1618
  def auto_devops_variables
    return [] unless auto_devops_enabled?

    auto_devops&.variables || []
  end

1619
  def append_or_update_attribute(name, value)
1620
    old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
1621 1622 1623 1624 1625 1626

    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
1627 1628 1629

  rescue ActiveRecord::RecordNotSaved => e
    handle_update_attribute_error(e, value)
1630 1631
  end

1632
  def pushes_since_gc
1633
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
1634 1635 1636
  end

  def increment_pushes_since_gc
1637
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
1638 1639 1640
  end

  def reset_pushes_since_gc
1641
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
1642 1643
  end

Douwe Maan's avatar
Douwe Maan committed
1644
  def route_map_for(commit_sha)
1645 1646
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
Douwe Maan's avatar
Douwe Maan committed
1647
        data = repository.route_map_for(sha)
1648
        next unless data
1649

Douwe Maan's avatar
Douwe Maan committed
1650 1651 1652
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1653
      end
1654
    end
1655 1656

    @route_maps_by_commit[commit_sha]
1657 1658
  end

1659
  def public_path_for_source_path(path, commit_sha)
Douwe Maan's avatar
Douwe Maan committed
1660
    map = route_map_for(commit_sha)
1661 1662
    return unless map

Douwe Maan's avatar
Douwe Maan committed
1663
    map.public_path_for_source_path(path)
1664 1665
  end

1666 1667 1668 1669
  def parent_changed?
    namespace_id_changed?
  end

1670 1671 1672 1673 1674 1675 1676 1677
  def default_merge_request_target
    if forked_from_project&.merge_requests_enabled?
      forked_from_project
    else
      self
    end
  end

1678 1679 1680
  def multiple_issue_boards_available?(user)
    feature_available?(:multiple_issue_boards, user)
  end
1681

1682 1683 1684 1685
  def full_path_was
    File.join(namespace.full_path, previous_changes['path'].first)
  end

1686 1687
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
1688
  # @deprecated cannot remove yet because it has an index with its name in elasticsearch
1689 1690
  alias_method :path_with_namespace, :full_path

1691 1692 1693 1694
  def forks_count
    Projects::ForksCountService.new(self).count
  end

1695
  def legacy_storage?
1696 1697 1698
    [nil, 0].include?(self.storage_version)
  end

1699 1700 1701 1702
  # 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)
1703 1704 1705
    raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)

    self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
1706 1707
  end

1708 1709 1710 1711
  def renamed?
    persisted? && path_changed?
  end

1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739
  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

1740
  def migrate_to_hashed_storage!
1741
    return if hashed_storage?(:repository)
1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761

    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

1762 1763 1764 1765
  def reference_counter(wiki: false)
    Gitlab::ReferenceCounter.new(gl_repository(is_wiki: wiki))
  end

1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776
  # 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

1777 1778
  private

1779 1780
  def storage
    @storage ||=
1781
      if hashed_storage?(:repository)
1782 1783 1784 1785 1786
        Storage::HashedProject.new(self)
      else
        Storage::LegacyProject.new(self)
      end
  end
1787

1788
  def use_hashed_storage
1789
    if self.new_record? && current_application_settings.hashed_storage_enabled
1790
      self.storage_version = LATEST_STORAGE_VERSION
1791 1792 1793
    end
  end

1794
  def repo_reference_count
1795
    reference_counter.value
1796 1797 1798
  end

  def wiki_reference_count
1799
    reference_counter(wiki: true).value
1800 1801
  end

1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814
  def check_repository_absence!
    return if skip_disk_validation

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

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

1815 1816 1817 1818 1819 1820 1821 1822 1823
  # 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

1824
  def cross_namespace_reference?(from)
1825 1826 1827 1828 1829
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
1830 1831 1832
    end
  end

1833
  # Check if a reference is being done cross-project
1834 1835 1836 1837
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
1838 1839
  end

1840
  def pushes_since_gc_redis_shared_state_key
1841 1842 1843
    "projects/#{id}/pushes_since_gc"
  end

1844 1845 1846 1847 1848 1849 1850
  # 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.
1851
  def validate_board_limit(board)
1852
    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
1853
  end
1854 1855 1856 1857 1858

  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
James Lopez's avatar
James Lopez committed
1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873

  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

1874
    Project.pending_delete.find_by_full_path(full_path)
James Lopez's avatar
James Lopez committed
1875
  end
1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886

  ##
  # 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
1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898

  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
gitlabhq's avatar
gitlabhq committed
1899
end