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

G
gitlabhq 已提交
3
class Project < ActiveRecord::Base
4
  include Gitlab::ConfigHelper
5
  include Gitlab::ShellAdapter
6
  include Gitlab::VisibilityLevel
7
  include Gitlab::CurrentSettings
R
Rémy Coutable 已提交
8
  include AccessRequestable
9
  include Avatarable
10
  include CacheMarkdownField
11 12
  include Referable
  include Sortable
13
  include AfterCommitQueue
14
  include CaseSensitivity
15
  include TokenAuthenticatable
16
  include ValidAttribute
F
Felipe Artur 已提交
17
  include ProjectFeaturesCompatibility
18
  include SelectForProjectAuthorization
19
  include Routable
R
Robert Speicher 已提交
20

21
  extend Gitlab::ConfigHelper
22

23
  BoardLimitExceeded = Class.new(StandardError)
24

25
  NUMBER_OF_PERMITTED_BOARDS = 1
D
Douwe Maan 已提交
26
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
J
Jared Szechy 已提交
27

28 29
  cache_markdown_field :description, pipeline: :description

30 31
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
32
                                                       allow_nil: true
F
Felipe Artur 已提交
33

34
  default_value_for :archived, false
35
  default_value_for :visibility_level, gitlab_config_features.visibility_level
36
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
37
  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
K
Kamil Trzcinski 已提交
38
  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
F
Felipe Artur 已提交
39 40 41 42 43
  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
44
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
45

46
  after_create :ensure_dir_exist
47
  after_create :create_project_feature, unless: :project_feature
48
  after_save :ensure_dir_exist, if: :namespace_id_changed?
M
Markus Koller 已提交
49
  after_save :update_project_statistics, if: :namespace_id_changed?
50

51 52
  # set last_activity_at to the same as created_at
  after_create :set_last_activity_at
53
  def set_last_activity_at
54
    update_column(:last_activity_at, self.created_at)
55 56
  end

57 58 59 60 61
  after_create :set_last_repository_updated_at
  def set_last_repository_updated_at
    update_column(:last_repository_updated_at, self.created_at)
  end

62
  before_destroy :remove_private_deploy_keys
K
Kamil Trzcinski 已提交
63 64
  after_destroy :remove_pages

65
  # update visibility_level of forks
66 67
  after_update :update_forks_visibility_level

68 69
  after_validation :check_pending_delete

70
  acts_as_taggable
D
Dmitriy Zaporozhets 已提交
71

72
  attr_accessor :new_default_branch
73
  attr_accessor :old_path_with_namespace
74
  attr_writer :pipeline_status
75

76 77
  alias_attribute :title, :name

78
  # Relations
79
  belongs_to :creator, class_name: 'User'
80
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
81
  belongs_to :namespace
82

83
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
84
  has_many :boards, before_add: :validate_board_limit
F
Felipe Artur 已提交
85

86
  # Project services
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
  has_one :campfire_service
  has_one :drone_ci_service
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
  has_one :hipchat_service
  has_one :flowdock_service
  has_one :assembla_service
  has_one :asana_service
  has_one :gemnasium_service
  has_one :mattermost_slash_commands_service
  has_one :mattermost_service
  has_one :slack_slash_commands_service
  has_one :slack_service
  has_one :buildkite_service
  has_one :bamboo_service
  has_one :teamcity_service
  has_one :pushover_service
  has_one :jira_service
  has_one :redmine_service
  has_one :custom_issue_tracker_service
  has_one :bugzilla_service
  has_one :gitlab_issue_tracker_service, inverse_of: :project
  has_one :external_wiki_service
  has_one :kubernetes_service, inverse_of: :project
  has_one :prometheus_service, inverse_of: :project
  has_one :mock_ci_service
  has_one :mock_deployment_service
  has_one :mock_monitoring_service
  has_one :microsoft_teams_service

  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
120 121 122 123
  has_one  :forked_from_project,  through:   :forked_project_link

  has_many :forked_project_links, foreign_key: "forked_from_project_id"
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
124

125
  # Merge Requests for target project should be removed with it
126 127 128 129 130 131 132 133 134 135 136
  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
137

138
  has_many :project_authorizations
139
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
140
  has_many :project_members, -> { where(requested_at: nil) },
141
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
142

R
Rémy Coutable 已提交
143
  alias_method :members, :project_members
144 145
  has_many :users, through: :project_members

146
  has_many :requesters, -> { where.not(requested_at: nil) },
147
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
148

149
  has_many :deploy_keys_projects
150
  has_many :deploy_keys, through: :deploy_keys_projects
151
  has_many :users_star_projects
C
Ciro Santilli 已提交
152
  has_many :starrers, through: :users_star_projects, source: :user
153
  has_many :releases
154
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
M
Marin Jankovski 已提交
155
  has_many :lfs_objects, through: :lfs_objects_projects
156
  has_many :project_group_links
157
  has_many :invited_groups, through: :project_group_links, source: :group
158 159
  has_many :pages_domains
  has_many :todos
160
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
161 162 163 164

  has_one :import_data, class_name: 'ProjectImportData'
  has_one :project_feature
  has_one :statistics, class_name: 'ProjectStatistics'
165

166 167 168
  # 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.
169
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
K
Kamil Trzcinski 已提交
170

171 172 173 174 175 176 177
  has_many :commit_statuses
  has_many :pipelines, class_name: 'Ci::Pipeline'

  # 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.
178
  has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
179
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
180
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
181
  has_many :variables, class_name: 'Ci::Variable'
182 183 184 185
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
K
Kamil Trzcinski 已提交
186

K
Kamil Trzcinski 已提交
187 188
  has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'

189
  accepts_nested_attributes_for :variables, allow_destroy: true
F
Felipe Artur 已提交
190
  accepts_nested_attributes_for :project_feature
191

192
  delegate :name, to: :owner, allow_nil: true, prefix: true
D
Douwe Maan 已提交
193
  delegate :count, to: :forks, prefix: true
194
  delegate :members, to: :team, prefix: true
195
  delegate :add_user, :add_users, to: :team
196
  delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
D
Douwe Maan 已提交
197
  delegate :empty_repo?, to: :repository
198

A
Andrey Kumanyaev 已提交
199
  # Validations
200
  validates :creator, presence: true, on: :create
201
  validates :description, length: { maximum: 2000 }, allow_blank: true
202
  validates :ci_config_path,
203
    format: { without: /\.{2}/,
204
              message: 'cannot include directory traversal.' },
205 206
    length: { maximum: 255 },
    allow_blank: true
207 208
  validates :name,
    presence: true,
209
    length: { maximum: 255 },
210
    format: { with: Gitlab::Regex.project_name_regex,
D
Douwe Maan 已提交
211
              message: Gitlab::Regex.project_name_regex_message }
212 213
  validates :path,
    presence: true,
214
    dynamic_path: true,
215
    length: { maximum: 255 },
216 217
    format: { with: Gitlab::PathRegex.project_path_format_regex,
              message: Gitlab::PathRegex.project_path_format_message },
218 219
    uniqueness: { scope: :namespace_id }

220
  validates :namespace, presence: true
D
Douwe Maan 已提交
221
  validates :name, uniqueness: { scope: :namespace_id }
J
James Lopez 已提交
222
  validates :import_url, addressable_url: true, if: :external_import?
D
Douwe Maan 已提交
223
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
224
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
225
  validate :check_limit, on: :create
226
  validate :avatar_type,
227
    if: ->(project) { project.avatar.present? && project.avatar_changed? }
228
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
D
Douwe Maan 已提交
229
  validate :visibility_level_allowed_by_group
D
Douwe Maan 已提交
230
  validate :visibility_level_allowed_as_fork
231
  validate :check_wiki_path_conflict
232 233 234
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
235

236
  add_authentication_token_field :runners_token
237
  before_save :ensure_runners_token
K
Kamil Trzcinski 已提交
238

D
Douwe Maan 已提交
239
  mount_uploader :avatar, AvatarUploader
240
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
A
Andrey Kumanyaev 已提交
241

242
  # Scopes
243
  scope :pending_delete, -> { where(pending_delete: true) }
244
  scope :without_deleted, -> { where(pending_delete: false) }
245

246
  scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
247 248
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

249
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
D
Dmitriy Zaporozhets 已提交
250
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
251
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
T
Toon Claes 已提交
252
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
R
Rémy Coutable 已提交
253
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
254
  scope :non_archived, -> { where(archived: false) }
R
Rubén Dávila 已提交
255
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
R
Rémy Coutable 已提交
256 257
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }

258
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
M
Markus Koller 已提交
259
  scope :with_statistics, -> { includes(:statistics) }
260
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
261 262 263
  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.
264 265
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
266
  end
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

  # "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) }
282
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
283

284
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
285

286 287 288 289
  # 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
290 291 292 293
      authorized = user
        .project_authorizations
        .select(1)
        .where('project_authorizations.project_id = projects.id')
294 295 296 297 298 299 300 301 302

      levels = Gitlab::VisibilityLevel.levels_for_user(user)

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

303 304 305
  # 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.
306 307 308 309
  #
  # 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.
310
  def self.with_feature_available_for_user(feature, user)
311 312 313 314 315 316 317
    visible = [nil, ProjectFeature::ENABLED]

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

318 319
      authorized = user.project_authorizations.select(1)
        .where('project_authorizations.project_id = projects.id')
320

321 322
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
323 324 325 326 327 328
              visible,
              ProjectFeature::PRIVATE,
              authorized)
    else
      with_feature_access_level(feature, visible)
    end
329
  end
F
Felipe Artur 已提交
330

R
Rémy Coutable 已提交
331 332
  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) }
333

334 335
  scope :excluding_project, ->(project) { where.not(id: project) }

336
  state_machine :import_status, initial: :none do
337 338 339 340 341 342 343 344
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

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

345
    event :import_start do
346
      transition scheduled: :started
347 348 349
    end

    event :import_finish do
350
      transition started: :finished
351 352 353
    end

    event :import_fail do
354
      transition [:scheduled, :started] => :failed
355 356 357
    end

    event :import_retry do
358
      transition failed: :started
359 360
    end

361
    state :scheduled
362 363
    state :started
    state :finished
364 365
    state :failed

366 367 368 369
    after_transition [:none, :finished, :failed] => :scheduled do |project, _|
      project.run_after_commit { add_import_job }
    end

370 371
    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs
372 373 374 375 376 377 378 379 380 381

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        project.run_after_commit do
          begin
            Projects::HousekeepingService.new(project).execute
          rescue Projects::HousekeepingService::LeaseTaken => e
            Rails.logger.info("Could not perform housekeeping for project #{project.path_with_namespace} (#{project.id}): #{e}")
          end
        end
      end
382
    end
383 384
  end

A
Andrey Kumanyaev 已提交
385
  class << self
386 387 388 389 390 391 392
    # 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.
393
    def search(query)
394
      ptable  = arel_table
395 396 397
      ntable  = Namespace.arel_table
      pattern = "%#{query}%"

M
mhasbini 已提交
398 399 400
      # unscoping unnecessary conditions that'll be applied
      # when executing `where("projects.id IN (#{union.to_sql})")`
      projects = unscoped.select(:id).where(
401 402 403
        ptable[:path].matches(pattern)
          .or(ptable[:name].matches(pattern))
          .or(ptable[:description].matches(pattern))
404 405
      )

406 407 408
      namespaces = unscoped.select(:id)
        .joins(:namespace)
        .where(ntable[:name].matches(pattern))
409 410 411 412

      union = Gitlab::SQL::Union.new([projects, namespaces])

      where("projects.id IN (#{union.to_sql})")
A
Andrey Kumanyaev 已提交
413
    end
414

415
    def search_by_title(query)
416 417 418 419
      pattern = "%#{query}%"
      table   = Project.arel_table

      non_archived.where(table[:name].matches(pattern))
420 421
    end

422 423 424
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
425 426

    def sort(method)
427 428
      case method.to_s
      when 'storage_size_desc'
M
Markus Koller 已提交
429 430 431
        # 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')
432 433 434 435
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
436 437
      else
        order_by(method)
438 439
      end
    end
440 441

    def reference_pattern
442
      %r{
443 444
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
445
      }x
446
    end
Y
Yorick Peterse 已提交
447

Y
Yorick Peterse 已提交
448
    def trending
449 450
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
Y
Yorick Peterse 已提交
451
    end
452 453 454 455 456 457

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

    def group_ids
460
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
461
    end
462 463
  end

464
  def lfs_enabled?
465
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
P
Patricio Cano 已提交
466

467
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
468 469
  end

470
  def repository_storage_path
471
    Gitlab.config.repositories.storages[repository_storage]['path']
472 473
  end

D
Dmitriy Zaporozhets 已提交
474
  def team
475
    @team ||= ProjectTeam.new(self)
D
Dmitriy Zaporozhets 已提交
476 477 478
  end

  def repository
479
    @repository ||= Repository.new(path_with_namespace, self)
480 481
  end

A
Andre Guedes 已提交
482
  def container_registry_url
K
Kamil Trzcinski 已提交
483
    if Gitlab.config.registry.enabled
484
      "#{Gitlab.config.registry.host_port}/#{path_with_namespace.downcase}"
485
    end
486 487
  end

488
  def has_container_registry_tags?
489 490 491
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
492
      has_root_container_repository_tags?
493 494
  end

495 496
  def commit(ref = 'HEAD')
    repository.commit(ref)
D
Dmitriy Zaporozhets 已提交
497 498
  end

499
  # ref can't be HEAD, can only be branch/tag name or SHA
500
  def latest_successful_builds_for(ref = default_branch)
501
    latest_pipeline = pipelines.latest_successful_for(ref)
502 503 504 505 506 507

    if latest_pipeline
      latest_pipeline.builds.latest.with_artifacts
    else
      builds.none
    end
508 509
  end

510
  def merge_base_commit(first_commit_id, second_commit_id)
D
Douwe Maan 已提交
511 512
    sha = repository.merge_base(first_commit_id, second_commit_id)
    repository.commit(sha) if sha
513 514
  end

515
  def saved?
516
    id && persisted?
517 518
  end

519
  def add_import_job
D
Douwe Maan 已提交
520 521 522 523 524 525 526 527
    job_id =
      if forked?
        RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
          forked_from_project.path_with_namespace,
          self.namespace.full_path)
      else
        RepositoryImportWorker.perform_async(self.id)
      end
528 529 530 531 532

    if job_id
      Rails.logger.info "Import job started for #{path_with_namespace} with job ID #{job_id}"
    else
      Rails.logger.error "Import job failed to start for #{path_with_namespace}"
533
    end
534 535
  end

536 537 538 539 540
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

541 542 543 544 545
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
546
    import_data&.destroy
547 548
  end

549
  def ci_config_path=(value)
550
    # Strip all leading slashes so that //foo -> foo
L
Lin Jen-Shin 已提交
551
    super(value&.sub(%r{\A/+}, '')&.delete("\0"))
552 553
  end

J
James Lopez 已提交
554
  def import_url=(value)
J
James Lopez 已提交
555 556
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

557
    import_url = Gitlab::UrlSanitizer.new(value)
J
James Lopez 已提交
558
    super(import_url.sanitized_url)
559
    create_or_update_import_data(credentials: import_url.credentials)
J
James Lopez 已提交
560 561 562
  end

  def import_url
J
James Lopez 已提交
563
    if import_data && super.present?
564
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
J
James Lopez 已提交
565 566 567
      import_url.full_url
    else
      super
J
James Lopez 已提交
568 569
    end
  end
J
James Lopez 已提交
570

J
James Lopez 已提交
571 572 573 574
  def valid_import_url?
    valid? || errors.messages[:import_url].nil?
  end

575
  def create_or_update_import_data(data: nil, credentials: nil)
576
    return unless import_url.present? && valid_import_url?
577

J
James Lopez 已提交
578
    project_import_data = import_data || build_import_data
J
James Lopez 已提交
579 580 581 582
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
583 584 585 586
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
587 588

    project_import_data.save
J
James Lopez 已提交
589
  end
J
James Lopez 已提交
590

591
  def import?
592
    external_import? || forked? || gitlab_project_import?
593 594
  end

595 596 597 598
  def no_import?
    import_status == 'none'
  end

599
  def external_import?
600 601 602
    import_url.present?
  end

603
  def imported?
604 605 606 607
    import_finished?
  end

  def import_in_progress?
608 609 610 611
    import_started? || import_scheduled?
  end

  def import_started?
612 613 614
    import? && import_status == 'started'
  end

615 616 617 618
  def import_scheduled?
    import_status == 'scheduled'
  end

619 620 621 622 623 624
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
625 626
  end

D
Douwe Maan 已提交
627
  def safe_import_url
628
    Gitlab::UrlSanitizer.new(import_url).masked_url
D
Douwe Maan 已提交
629 630
  end

631 632 633 634
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

R
Rémy Coutable 已提交
635 636 637 638
  def gitea_import?
    import_type == 'gitea'
  end

639 640 641 642
  def github_import?
    import_type == 'github'
  end

643
  def check_limit
D
Douwe Maan 已提交
644
    unless creator.can_create_project? || namespace.kind == 'group'
645 646 647
      projects_limit = creator.projects_limit

      if projects_limit == 0
P
Phil Hughes 已提交
648
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
649
      else
P
Phil Hughes 已提交
650
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
651
      end
652 653
    end
  rescue
D
Douwe Maan 已提交
654
    self.errors.add(:base, "Can't check your ability to create project")
G
gitlabhq 已提交
655 656
  end

D
Douwe Maan 已提交
657 658 659 660 661 662 663 664 665 666 667 668 669
  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.")
670 671
  end

672 673 674 675 676 677 678 679 680 681
  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

682
  def to_param
683 684 685 686 687
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
688 689
  end

690
  # `from` argument can be a Namespace or Project.
691 692 693 694 695 696
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
      path_with_namespace
    elsif cross_project_reference?(from)
      path
    end
697 698 699 700 701 702 703 704
  end

  def to_human_reference(from_project = nil)
    if cross_namespace_reference?(from_project)
      name_with_namespace
    elsif cross_project_reference?(from_project)
      name
    end
705 706
  end

707
  def web_url
708
    Gitlab::Routing.url_helpers.project_url(self)
709 710
  end

711
  def new_issue_address(author)
712
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
713

714 715 716 717
    author.ensure_incoming_email_token!

    Gitlab::IncomingEmail.reply_address(
      "#{path_with_namespace}+#{author.incoming_email_token}")
718 719
  end

720
  def build_commit_note(commit)
721
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
G
gitlabhq 已提交
722
  end
N
Nihad Abbasov 已提交
723

N
Nihad Abbasov 已提交
724
  def last_activity
725
    last_event
G
gitlabhq 已提交
726 727 728
  end

  def last_activity_date
729
    last_repository_updated_at || last_activity_at || updated_at
D
Dmitriy Zaporozhets 已提交
730
  end
731

D
Dmitriy Zaporozhets 已提交
732 733 734
  def project_id
    self.id
  end
R
randx 已提交
735

736
  def get_issue(issue_id, current_user)
737 738 739 740 741
    issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?

    if issue
      issue
    elsif external_issue_tracker
R
Robert Speicher 已提交
742
      ExternalIssue.new(issue_id, self)
743 744 745
    end
  end

R
Robert Speicher 已提交
746
  def issue_exists?(issue_id)
747
    get_issue(issue_id)
R
Robert Speicher 已提交
748 749
  end

750
  def default_issue_tracker
751
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
752 753 754 755 756 757 758 759 760 761
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

762
  def external_issue_reference_pattern
763
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
764 765
  end

766
  def default_issues_tracker?
767
    !external_issue_tracker
768 769 770
  end

  def external_issue_tracker
771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
    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
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
786 787
  end

788 789 790 791
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807
  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
    update_column(:has_external_wiki, services.external_wikis.any?)
  end

808
  def find_or_initialize_services(exceptions: [])
M
Marin Jankovski 已提交
809 810
    services_templates = Service.where(template: true)

811 812 813
    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
M
Marin Jankovski 已提交
814
      service = find_service(services, service_name)
815

816 817 818
      if service
        service
      else
M
Marin Jankovski 已提交
819 820 821 822
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
823 824
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
          public_send("build_#{service_name}_service")
M
Marin Jankovski 已提交
825
        else
826
          Service.build_from_template(id, template)
M
Marin Jankovski 已提交
827 828
        end
      end
829 830 831
    end
  end

832 833 834 835
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

V
Valery Sizov 已提交
836 837
  def create_labels
    Label.templates.each do |label|
838
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
839
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
V
Valery Sizov 已提交
840 841 842
    end
  end

M
Marin Jankovski 已提交
843 844 845
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
D
Dmitriy Zaporozhets 已提交
846

847
  def ci_services
Y
Yorick Peterse 已提交
848
    services.where(category: :ci)
849 850 851
  end

  def ci_service
852
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
853 854
  end

855 856 857 858 859
  def deployment_services
    services.where(category: :deployment)
  end

  def deployment_service
860
    @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
861 862
  end

863 864 865 866 867
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
868
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
869 870
  end

D
Drew Blessing 已提交
871 872 873 874
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

875
  def avatar_type
876 877
    unless self.avatar.image?
      self.errors.add :avatar, 'only images allowed'
878 879 880 881
    end
  end

  def avatar_in_git
882
    repository.avatar
883 884
  end

885 886 887
  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
888
    avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
S
sue445 已提交
889 890
  end

891 892 893 894 895
  # For compatibility with old code
  def code
    path
  end

896
  def items_for(entity)
D
Dmitriy Zaporozhets 已提交
897 898 899 900 901 902 903
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
904

905
  def send_move_instructions(old_path_with_namespace)
906 907 908
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
    run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
909
  end
910 911

  def owner
912 913
    if group
      group
914
    else
915
      namespace.try(:owner)
916 917
    end
  end
D
Dmitriy Zaporozhets 已提交
918

919 920
  def execute_hooks(data, hooks_scope = :push_hooks)
    hooks.send(hooks_scope).each do |hook|
921
      hook.async_execute(data, hooks_scope.to_s)
922
    end
D
Dmitriy Zaporozhets 已提交
923 924
  end

925 926 927
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
    services.send(hooks_scope).each do |service|
928
      service.async_execute(data)
D
Dmitriy Zaporozhets 已提交
929 930 931 932
    end
  end

  def valid_repo?
933
    repository.exists?
D
Dmitriy Zaporozhets 已提交
934
  rescue
935
    errors.add(:path, 'Invalid repository path')
D
Dmitriy Zaporozhets 已提交
936 937 938 939
    false
  end

  def repo
D
Dmitriy Zaporozhets 已提交
940
    repository.raw
D
Dmitriy Zaporozhets 已提交
941 942 943
  end

  def url_to_repo
944
    gitlab_shell.url_to_repo(path_with_namespace)
D
Dmitriy Zaporozhets 已提交
945 946 947
  end

  def repo_exists?
948
    @repo_exists ||= repository.exists?
D
Dmitriy Zaporozhets 已提交
949 950 951 952 953
  rescue
    @repo_exists = false
  end

  def root_ref?(branch)
D
Dmitriy Zaporozhets 已提交
954
    repository.root_ref == branch
D
Dmitriy Zaporozhets 已提交
955 956 957 958 959 960
  end

  def ssh_url_to_repo
    url_to_repo
  end

961 962
  def http_url_to_repo
    "#{web_url}.git"
D
Dmitriy Zaporozhets 已提交
963 964
  end

965
  def user_can_push_to_empty_repo?(user)
966
    !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
967 968
  end

969 970 971
  def forked?
    !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
  end
D
Dmitriy Zaporozhets 已提交
972

973 974 975 976
  def personal?
    !group
  end

D
Dmitriy Zaporozhets 已提交
977
  def rename_repo
978
    path_was = previous_changes['path'].first
979 980
    old_path_with_namespace = File.join(namespace.full_path, path_was)
    new_path_with_namespace = File.join(namespace.full_path, path)
D
Dmitriy Zaporozhets 已提交
981

982 983
    Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"

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

A
Andre Guedes 已提交
987 988
      # 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')
989 990
    end

991 992
    expire_caches_before_rename(old_path_with_namespace)

993
    if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
994
      # If repository moved successfully we need to send update instructions to users.
D
Dmitriy Zaporozhets 已提交
995 996 997
      # However we cannot allow rollback since we moved repository
      # So we basically we mute exceptions in next actions
      begin
998
        gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
999
        send_move_instructions(old_path_with_namespace)
1000
        expires_full_path_cache
1001 1002 1003 1004 1005

        @old_path_with_namespace = old_path_with_namespace

        SystemHooksService.new.execute_hooks_for(self, :rename)

1006
        @repository = nil
1007 1008
      rescue => e
        Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}"
J
Johannes Schleifenbaum 已提交
1009
        # Returning false does not rollback after_* transaction but gives
D
Dmitriy Zaporozhets 已提交
1010 1011 1012 1013
        # us information about failing some of tasks
        false
      end
    else
1014 1015
      Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"

D
Dmitriy Zaporozhets 已提交
1016 1017
      # if we cannot move namespace directory we should rollback
      # db changes in order to prevent out of sync between db and fs
1018
      raise StandardError.new('repository cannot be renamed')
D
Dmitriy Zaporozhets 已提交
1019
    end
1020

1021 1022
    Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"

1023 1024
    Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.full_path)
    Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.full_path)
D
Dmitriy Zaporozhets 已提交
1025
  end
1026

1027 1028 1029 1030 1031 1032
  # Expires various caches before a project is renamed.
  def expire_caches_before_rename(old_path)
    repo = Repository.new(old_path, self)
    wiki = Repository.new("#{old_path}.wiki", self)

    if repo.exists?
1033
      repo.before_delete
1034 1035 1036
    end

    if wiki.exists?
1037
      wiki.before_delete
1038 1039 1040
    end
  end

1041 1042
  def hook_attrs(backward: true)
    attrs = {
K
Kirill Zaitsev 已提交
1043
      name: name,
1044
      description: description,
K
Kirilll Zaitsev 已提交
1045
      web_url: web_url,
1046 1047 1048
      avatar_url: avatar_url,
      git_ssh_url: ssh_url_to_repo,
      git_http_url: http_url_to_repo,
K
Kirill Zaitsev 已提交
1049
      namespace: namespace.name,
1050 1051 1052
      visibility_level: visibility_level,
      path_with_namespace: path_with_namespace,
      default_branch: default_branch,
1053
      ci_config_path: ci_config_path
K
Kirill Zaitsev 已提交
1054
    }
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066

    # 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
K
Kirill Zaitsev 已提交
1067 1068
  end

1069
  def project_member(user)
G
Gabriel Mazetto 已提交
1070
    project_members.find_by(user_id: user)
1071
  end
1072 1073 1074 1075

  def default_branch
    @default_branch ||= repository.root_ref if repository.exists?
  end
1076 1077 1078 1079 1080

  def reload_default_branch
    @default_branch = nil
    default_branch
  end
1081

1082
  def visibility_level_field
1083
    :visibility_level
1084
  end
1085 1086 1087 1088 1089 1090 1091 1092

  def archive!
    update_attribute(:archived, true)
  end

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

1094
  def change_head(branch)
1095
    repository.before_change_head
P
P.S.V.R 已提交
1096 1097 1098
    repository.rugged.references.create('HEAD',
                                        "refs/heads/#{branch}",
                                        force: true)
1099
    repository.copy_gitattributes(branch)
1100
    repository.after_change_head
1101 1102
    reload_default_branch
  end
1103 1104 1105 1106

  def forked_from?(project)
    forked? && project == forked_from_project
  end
1107

1108 1109 1110
  def origin_merge_requests
    merge_requests.where(source_project_id: self.id)
  end
1111

1112
  def create_repository(force: false)
1113
    # Forked import is handled asynchronously
1114 1115 1116 1117 1118 1119 1120 1121
    return if forked? && !force

    if gitlab_shell.add_repository(repository_storage_path, path_with_namespace)
      repository.after_create
      true
    else
      errors.add(:base, 'Failed to create repository via gitlab-shell')
      false
1122 1123 1124
    end
  end

1125
  def ensure_repository
1126
    create_repository(force: true) unless repository_exists?
1127 1128
  end

1129 1130 1131 1132
  def repository_exists?
    !!repository.exists?
  end

1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
  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

1144 1145 1146
  def create_wiki
    ProjectWiki.new(self, self.owner).wiki
    true
G
Guilherme Garnier 已提交
1147
  rescue ProjectWiki::CouldNotCreateWikiError
1148
    errors.add(:base, 'Failed create wiki')
1149 1150
    false
  end
1151

1152 1153 1154 1155
  def wiki
    @wiki ||= ProjectWiki.new(self, self.owner)
  end

D
Drew Blessing 已提交
1156 1157 1158 1159
  def jira_tracker_active?
    jira_tracker? && jira_service.active
  end

1160
  def allowed_to_share_with_group?
1161
    !namespace.share_with_group_lock
1162 1163
  end

1164 1165 1166
  def pipeline_for(ref, sha = nil)
    sha ||= commit(ref).try(:sha)

1167
    return unless sha
1168

1169
    pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
K
Kamil Trzcinski 已提交
1170 1171
  end

1172
  def enable_ci
F
Felipe Artur 已提交
1173
    project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
1174
  end
M
Marin Jankovski 已提交
1175

1176 1177 1178 1179 1180
  def shared_runners_available?
    shared_runners_enabled?
  end

  def shared_runners
1181
    @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none
1182 1183
  end

1184 1185
  def active_shared_runners
    @active_shared_runners ||= shared_runners.active
1186
  end
K
Kamil Trzcinski 已提交
1187 1188

  def any_runners?(&block)
K
Kamil Trzcinski 已提交
1189
    active_runners.any?(&block) || active_shared_runners.any?(&block)
K
Kamil Trzcinski 已提交
1190 1191
  end

1192
  def valid_runners_token?(token)
J
James Lopez 已提交
1193
    self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
K
Kamil Trzcinski 已提交
1194 1195
  end

K
Kamil Trzcinski 已提交
1196 1197 1198 1199 1200 1201 1202
  def build_timeout_in_minutes
    build_timeout / 60
  end

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

S
Stan Hu 已提交
1204 1205 1206
  def open_issues_count
    issues.opened.count
  end
1207

D
Douwe Maan 已提交
1208
  def visibility_level_allowed_as_fork?(level = self.visibility_level)
D
Douwe Maan 已提交
1209
    return true unless forked?
D
Douwe Maan 已提交
1210

D
Douwe Maan 已提交
1211 1212 1213 1214 1215 1216
    # self.forked_from_project will be nil before the project is saved, so
    # we need to go through the relation
    original_project = forked_project_link.forked_from_project
    return true unless original_project

    level <= original_project.visibility_level
D
Douwe Maan 已提交
1217
  end
1218

D
Douwe Maan 已提交
1219 1220
  def visibility_level_allowed_by_group?(level = self.visibility_level)
    return true unless group
1221

D
Douwe Maan 已提交
1222
    level <= group.visibility_level
M
Marin Jankovski 已提交
1223
  end
1224

D
Douwe Maan 已提交
1225 1226
  def visibility_level_allowed?(level = self.visibility_level)
    visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
F
Felipe Artur 已提交
1227 1228
  end

1229 1230 1231
  def runners_token
    ensure_runners_token!
  end
V
Valery Sizov 已提交
1232

1233 1234 1235
  def pages_deployed?
    Dir.exist?(public_pages_path)
  end
1236

1237
  def pages_url
1238 1239
    subdomain, _, url_path = full_path.partition('/')

K
Kamil Trzcinski 已提交
1240 1241
    # The hostname always needs to be in downcased
    # All web servers convert hostname to lowercase
1242
    host = "#{subdomain}.#{Settings.pages.host}".downcase
K
Kamil Trzcinski 已提交
1243 1244

    # The host in URL always needs to be downcased
1245
    url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
1246
      "#{prefix}#{subdomain}."
K
Kamil Trzcinski 已提交
1247
    end.downcase
1248

K
Kamil Trzcinski 已提交
1249
    # If the project path is the same as host, we serve it as group page
1250 1251 1252 1253
    return url if host == url_path

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

1255 1256
  def pages_subdomain
    full_path.partition('/').first
1257
  end
K
Kamil Trzcinski 已提交
1258 1259

  def pages_path
1260
    File.join(Settings.pages.path, path_with_namespace)
K
Kamil Trzcinski 已提交
1261 1262 1263 1264 1265 1266
  end

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

1267 1268 1269 1270
  def remove_private_deploy_keys
    deploy_keys.where(public: false).delete_all
  end

K
Kamil Trzcinski 已提交
1271
  def remove_pages
1272 1273
    ::Projects::UpdatePagesConfigurationService.new(self).execute

1274 1275 1276
    # 1. We rename pages to temporary directory
    # 2. We wait 5 minutes, due to NFS caching
    # 3. We asynchronously remove pages with force
K
Kamil Trzcinski 已提交
1277
    temp_path = "#{path}.#{SecureRandom.hex}.deleted"
K
Kamil Trzcinski 已提交
1278

1279 1280
    if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
      PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
K
Kamil Trzcinski 已提交
1281
    end
K
Kamil Trzcinski 已提交
1282 1283
  end

J
Josh Frye 已提交
1284 1285
  def running_or_pending_build_count(force: false)
    Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
1286 1287 1288
      builds.running_or_pending.count(:all)
    end
  end
J
James Lopez 已提交
1289

1290
  # Lazy loading of the `pipeline_status` attribute
1291
  def pipeline_status
1292
    @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
1293 1294
  end

J
James Lopez 已提交
1295
  def mark_import_as_failed(error_message)
1296 1297 1298
    original_errors = errors.dup
    sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)

J
James Lopez 已提交
1299
    import_fail
1300 1301 1302 1303 1304
    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
J
James Lopez 已提交
1305
  end
J
James Lopez 已提交
1306

1307 1308
  def add_export_job(current_user:)
    job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
1309 1310 1311 1312 1313 1314 1315

    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
J
James Lopez 已提交
1316 1317

  def export_path
1318
    File.join(Gitlab::ImportExport.storage_path, path_with_namespace)
J
James Lopez 已提交
1319
  end
1320 1321 1322 1323 1324 1325 1326 1327 1328

  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
1329 1330

  def ensure_dir_exist
1331
    gitlab_shell.add_namespace(repository_storage_path, namespace.full_path)
1332
  end
1333 1334 1335 1336 1337 1338

  def predefined_variables
    [
      { key: 'CI_PROJECT_ID', value: id.to_s, public: true },
      { key: 'CI_PROJECT_NAME', value: path, public: true },
      { key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true },
1339
      { key: 'CI_PROJECT_PATH_SLUG', value: path_with_namespace.parameterize, public: true },
1340
      { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
      { key: 'CI_PROJECT_URL', value: web_url, public: true }
    ]
  end

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

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

K
Kamil Trzcinski 已提交
1352
    if container_registry_enabled?
A
Andre Guedes 已提交
1353
      variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true }
K
Kamil Trzcinski 已提交
1354 1355
    end

1356 1357 1358
    variables
  end

L
Lin Jen-Shin 已提交
1359 1360
  def secret_variables_for(ref:, environment: nil)
    # EE would use the environment
1361 1362 1363 1364 1365 1366
    if protected_for?(ref)
      variables
    else
      variables.unprotected
    end
  end
1367

1368 1369 1370
  def protected_for?(ref)
    ProtectedBranch.protected?(self, ref) ||
      ProtectedTag.protected?(self, ref)
1371
  end
1372

1373 1374 1375 1376 1377 1378
  def deployment_variables
    return [] unless deployment_service

    deployment_service.predefined_variables
  end

1379 1380 1381 1382 1383 1384 1385 1386
  def append_or_update_attribute(name, value)
    old_values = public_send(name.to_s)

    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
1387 1388 1389

  rescue ActiveRecord::RecordNotSaved => e
    handle_update_attribute_error(e, value)
1390 1391
  end

Y
Yorick Peterse 已提交
1392
  def pushes_since_gc
1393
    Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
Y
Yorick Peterse 已提交
1394 1395 1396
  end

  def increment_pushes_since_gc
1397
    Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
Y
Yorick Peterse 已提交
1398 1399 1400
  end

  def reset_pushes_since_gc
1401
    Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
Y
Yorick Peterse 已提交
1402 1403
  end

D
Douwe Maan 已提交
1404
  def route_map_for(commit_sha)
1405 1406
    @route_maps_by_commit ||= Hash.new do |h, sha|
      h[sha] = begin
D
Douwe Maan 已提交
1407
        data = repository.route_map_for(sha)
1408 1409
        next unless data

D
Douwe Maan 已提交
1410 1411 1412
        Gitlab::RouteMap.new(data)
      rescue Gitlab::RouteMap::FormatError
        nil
1413 1414 1415 1416 1417 1418 1419
      end
    end

    @route_maps_by_commit[commit_sha]
  end

  def public_path_for_source_path(path, commit_sha)
D
Douwe Maan 已提交
1420
    map = route_map_for(commit_sha)
1421 1422
    return unless map

D
Douwe Maan 已提交
1423
    map.public_path_for_source_path(path)
1424 1425
  end

1426 1427 1428 1429 1430 1431 1432 1433
  def parent
    namespace
  end

  def parent_changed?
    namespace_id_changed?
  end

1434 1435 1436 1437 1438 1439 1440 1441
  def default_merge_request_target
    if forked_from_project&.merge_requests_enabled?
      forked_from_project
    else
      self
    end
  end

1442 1443 1444 1445
  alias_method :name_with_namespace, :full_name
  alias_method :human_name, :full_name
  alias_method :path_with_namespace, :full_path

1446 1447
  private

1448
  def cross_namespace_reference?(from)
1449 1450 1451 1452 1453
    case from
    when Project
      namespace != from.namespace
    when Namespace
      namespace != from
1454 1455 1456
    end
  end

1457
  # Check if a reference is being done cross-project
1458 1459 1460 1461
  def cross_project_reference?(from)
    return true if from.is_a?(Namespace)

    from && self != from
1462 1463
  end

1464
  def pushes_since_gc_redis_shared_state_key
Y
Yorick Peterse 已提交
1465 1466 1467
    "projects/#{id}/pushes_since_gc"
  end

1468 1469 1470 1471 1472 1473 1474
  # 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.
1475
  def validate_board_limit(board)
1476
    raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
1477
  end
1478

M
Markus Koller 已提交
1479 1480 1481 1482
  def update_project_statistics
    stats = statistics || build_statistics
    stats.update(namespace_id: namespace_id)
  end
1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497

  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

1498
    Project.pending_delete.find_by_full_path(path_with_namespace)
1499
  end
1500 1501 1502 1503 1504 1505 1506 1507 1508

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

1509
    ContainerRepository.build_root_repository(self).has_tags?
1510
  end
1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522

  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
G
gitlabhq 已提交
1523
end