This instance will be upgraded to Heptapod 0.26.0rc1 on 2021-10-26 at 14:30 UTC+2 (a few minutes of down time)

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

gitlabhq's avatar
gitlabhq committed
3
class Project < ActiveRecord::Base
4
  include Gitlab::ConfigHelper
5
  include Gitlab::ShellAdapter
6
  include Gitlab::VisibilityLevel
7
  include Gitlab::CurrentSettings
Rémy Coutable's avatar
Rémy Coutable committed
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
Felipe Artur's avatar
Felipe Artur committed
17
  include ProjectFeaturesCompatibility
18
  include SelectForProjectAuthorization
19
  include Routable
Robert Speicher's avatar
Robert Speicher committed
20

21
  extend Gitlab::ConfigHelper
22
  extend Gitlab::CurrentSettings
23

24
  BoardLimitExceeded = Class.new(StandardError)
25

26
  NUMBER_OF_PERMITTED_BOARDS = 1
Douwe Maan's avatar
Douwe Maan committed
27
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
28
  LATEST_STORAGE_VERSION = 1
Jared Szechy's avatar
Jared Szechy committed
29

30
31
  cache_markdown_field :description, pipeline: :description

32
33
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
34
                                                       allow_nil: true
Felipe Artur's avatar
Felipe Artur committed
35

36
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
37

38
  default_value_for :archived, false
39
  default_value_for :visibility_level, gitlab_config_features.visibility_level
40
  default_value_for :resolve_outdated_diff_discussions, false
41
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
42
  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
43
  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
44
45
46
47
48
  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
49
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
50

51
52
  add_authentication_token_field :runners_token
  before_save :ensure_runners_token
53

54
55
  after_save :update_project_statistics, if: :namespace_id_changed?
  after_create :create_project_feature, unless: :project_feature
56
  after_create :set_last_activity_at
57
  after_create :set_last_repository_updated_at
58
  after_update :update_forks_visibility_level
59

60
  before_destroy :remove_private_deploy_keys
61
  after_destroy -> { run_after_commit { remove_pages } }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
62

63
64
  after_validation :check_pending_delete

65
  # Storage specific hooks
66
  after_initialize :use_hashed_storage
67
  after_create :check_repository_absence!
68
69
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
70

71
  acts_as_taggable
72

73
  attr_accessor :old_path_with_namespace
74
  attr_accessor :template_name
75
  attr_writer :pipeline_status
76
  attr_accessor :skip_disk_validation
77

78
79
  alias_attribute :title, :name

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

85
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
86
  has_many :boards, before_add: :validate_board_limit
Felipe Artur's avatar
Felipe Artur committed
87

88
  # Project services
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
120
121
  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"
122
123
124
125
  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
126

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

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

145
  alias_method :members, :project_members
146
147
  has_many :users, through: :project_members

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

152
  has_many :deploy_keys_projects
153
  has_many :deploy_keys, through: :deploy_keys_projects
154
  has_many :users_star_projects
Ciro Santilli's avatar
Ciro Santilli committed
155
  has_many :starrers, through: :users_star_projects, source: :user
156
  has_many :releases
157
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Marin Jankovski's avatar
Marin Jankovski committed
158
  has_many :lfs_objects, through: :lfs_objects_projects
159
  has_many :project_group_links
160
  has_many :invited_groups, through: :project_group_links, source: :group
161
162
  has_many :pages_domains
  has_many :todos
163
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
164

165
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
166
  has_one :project_feature, inverse_of: :project
167
  has_one :statistics, class_name: 'ProjectStatistics'
168

169
170
171
  # 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.
172
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
173

174
175
176
177
178
179
180
  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.
181
  has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
182
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
183
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
184
  has_many :variables, class_name: 'Ci::Variable'
185
186
187
188
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
189

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

192
193
  has_one :auto_devops, class_name: 'ProjectAutoDevops'

194
  accepts_nested_attributes_for :variables, allow_destroy: true
195
  accepts_nested_attributes_for :project_feature, update_only: true
196
  accepts_nested_attributes_for :import_data
197
  accepts_nested_attributes_for :auto_devops, update_only: true
198

199
  delegate :name, to: :owner, allow_nil: true, prefix: true
200
  delegate :members, to: :team, prefix: true
201
  delegate :add_user, :add_users, to: :team
202
  delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
Douwe Maan's avatar
Douwe Maan committed
203
  delegate :empty_repo?, to: :repository
204

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
205
  # Validations
206
  validates :creator, presence: true, on: :create
207
  validates :description, length: { maximum: 2000 }, allow_blank: true
208
  validates :ci_config_path,
209
    format: { without: /\.{2}/,
210
              message: 'cannot include directory traversal.' },
211
212
    length: { maximum: 255 },
    allow_blank: true
213
214
  validates :name,
    presence: true,
215
    length: { maximum: 255 },
216
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
217
              message: Gitlab::Regex.project_name_regex_message }
218
219
  validates :path,
    presence: true,
220
    dynamic_path: true,
221
    length: { maximum: 255 },
222
223
    format: { with: Gitlab::PathRegex.project_path_format_regex,
              message: Gitlab::PathRegex.project_path_format_message },
224
225
    uniqueness: { scope: :namespace_id }

226
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
227
  validates :name, uniqueness: { scope: :namespace_id }
228
  validates :import_url, addressable_url: true, if: :external_import?
229
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
230
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
231
  validate :check_limit, on: :create
232
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
233
  validate :avatar_type,
234
    if: ->(project) { project.avatar.present? && project.avatar_changed? }
235
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
Douwe Maan's avatar
Douwe Maan committed
236
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
237
  validate :visibility_level_allowed_as_fork
238
  validate :check_wiki_path_conflict
239
240
241
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
242

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

246
  # Scopes
247
  scope :pending_delete, -> { where(pending_delete: true) }
248
  scope :without_deleted, -> { where(pending_delete: false) }
249

250
251
252
  scope :with_hashed_storage, -> { where('storage_version >= 1') }
  scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }

253
  scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
254
255
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

256
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
257
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
258
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
259
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
Rémy Coutable's avatar
Rémy Coutable committed
260
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
261
  scope :archived, -> { where(archived: true) }
262
  scope :non_archived, -> { where(archived: false) }
263
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
Rémy Coutable's avatar
Rémy Coutable committed
264
265
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }

266
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
Markus Koller's avatar
Markus Koller committed
267
  scope :with_statistics, -> { includes(:statistics) }
268
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
269
270
271
  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.
272
273
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
274
  end
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289

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

292
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
293

294
295
296
297
  # 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
298
299
300
301
      authorized = user
        .project_authorizations
        .select(1)
        .where('project_authorizations.project_id = projects.id')
302
303
304
305
306
307
308
309
310

      levels = Gitlab::VisibilityLevel.levels_for_user(user)

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

311
312
313
  # 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.
314
315
316
317
  #
  # 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.
318
  def self.with_feature_available_for_user(feature, user)
319
320
321
322
323
324
325
    visible = [nil, ProjectFeature::ENABLED]

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

326
327
      authorized = user.project_authorizations.select(1)
        .where('project_authorizations.project_id = projects.id')
328

329
330
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
331
332
333
334
335
336
              visible,
              ProjectFeature::PRIVATE,
              authorized)
    else
      with_feature_access_level(feature, visible)
    end
337
  end
Felipe Artur's avatar
Felipe Artur committed
338

Rémy Coutable's avatar
Rémy Coutable committed
339
340
  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) }
341

342
343
  scope :excluding_project, ->(project) { where.not(id: project) }

344
  state_machine :import_status, initial: :none do
345
346
347
348
349
350
351
352
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

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

353
    event :import_start do
354
      transition scheduled: :started
355
356
357
    end

    event :import_finish do
358
      transition started: :finished
359
360
361
    end

    event :import_fail do
362
      transition [:scheduled, :started] => :failed
363
364
365
    end

    event :import_retry do
366
      transition failed: :started
367
368
    end

369
    state :scheduled
370
371
    state :started
    state :finished
372
373
    state :failed

374
    after_transition [:none, :finished, :failed] => :scheduled do |project, _|
375
376
377
378
      project.run_after_commit do
        job_id = add_import_job
        update(import_jid: job_id) if job_id
      end
379
380
    end

381
382
    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs
383
384
385

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        project.run_after_commit do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
386
          Projects::AfterImportService.new(project).execute
387
388
        end
      end
389
    end
390
391
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
392
  class << self
393
394
395
396
397
398
399
    # 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.
400
    def search(query)
401
      ptable  = arel_table
402
403
404
      ntable  = Namespace.arel_table
      pattern = "%#{query}%"

405
406
407
      # unscoping unnecessary conditions that'll be applied
      # when executing `where("projects.id IN (#{union.to_sql})")`
      projects = unscoped.select(:id).where(
408
409
410
        ptable[:path].matches(pattern)
          .or(ptable[:name].matches(pattern))
          .or(ptable[:description].matches(pattern))
411
412
      )

413
414
415
      namespaces = unscoped.select(:id)
        .joins(:namespace)
        .where(ntable[:name].matches(pattern))
416
417
418

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

419
      where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
420
    end
421

422
    def search_by_title(query)
423
424
425
426
      pattern = "%#{query}%"
      table   = Project.arel_table

      non_archived.where(table[:name].matches(pattern))
427
428
    end

429
430
431
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
432
433

    def sort(method)
434
435
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
436
437
438
        # 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')
439
440
441
442
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
443
444
      else
        order_by(method)
445
446
      end
    end
447
448

    def reference_pattern
449
      %r{
450
451
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
452
      }x
453
    end
454

455
    def trending
456
457
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
458
    end
459
460
461
462
463
464

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

    def group_ids
467
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
468
    end
469
470
  end

471
  def lfs_enabled?
472
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
473

474
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
475
476
  end

477
  def auto_devops_enabled?
478
    if auto_devops&.enabled.nil?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
479
      current_application_settings.auto_devops_enabled?
480
481
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
482
    end
483
484
  end

485
486
487
488
  def has_auto_devops_implicitly_disabled?
    auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
  end

489
  def repository_storage_path
490
    Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
491
492
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
493
  def team
494
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
495
496
497
  end

  def repository
498
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
499
500
  end

501
502
503
504
  def reload_repository!
    @repository = nil
  end

Andre Guedes's avatar
Andre Guedes committed
505
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
506
    if Gitlab.config.registry.enabled
507
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
508
    end
509
510
  end

511
  def has_container_registry_tags?
512
513
514
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
515
      has_root_container_repository_tags?
516
517
  end

518
519
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
520
521
  end

522
  # ref can't be HEAD, can only be branch/tag name or SHA
523
  def latest_successful_builds_for(ref = default_branch)
524
    latest_pipeline = pipelines.latest_successful_for(ref)
525
526
527
528
529
530

    if latest_pipeline
      latest_pipeline.builds.latest.with_artifacts
    else
      builds.none
    end
531
532
  end

533
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
534
535
    sha = repository.merge_base(first_commit_id, second_commit_id)
    repository.commit(sha) if sha
536
537
  end

538
  def saved?
539
    id && persisted?
540
541
  end

542
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
543
544
    job_id =
      if forked?
545
546
547
548
        RepositoryForkWorker.perform_async(id,
                                           forked_from_project.repository_storage_path,
                                           forked_from_project.full_path,
                                           self.namespace.full_path)
Douwe Maan's avatar
Douwe Maan committed
549
550
551
      else
        RepositoryImportWorker.perform_async(self.id)
      end
552

553
554
555
556
557
558
559
560
    log_import_activity(job_id)

    job_id
  end

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

561
    if job_id
562
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
563
    else
564
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
565
    end
566
567
  end

568
569
570
571
572
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

573
    update(import_error: nil)
574
575
576
577
578
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
579
    import_data&.destroy
580
581
  end

582
  def ci_config_path=(value)
583
    # Strip all leading slashes so that //foo -> foo
Lin Jen-Shin's avatar
Lin Jen-Shin committed
584
    super(value&.sub(%r{\A/+}, '')&.delete("\0"))
585
586
  end

587
  def import_url=(value)
James Lopez's avatar
James Lopez committed
588
589
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

590
    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
591
    super(import_url.sanitized_url)
592
    create_or_update_import_data(credentials: import_url.credentials)
593
594
595
  end

  def import_url
James Lopez's avatar
James Lopez committed
596
    if import_data && super.present?
597
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
598
599
600
      import_url.full_url
    else
      super
601
602
    end
  end
603

James Lopez's avatar
James Lopez committed
604
  def valid_import_url?
605
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
606
607
  end

608
  def create_or_update_import_data(data: nil, credentials: nil)
609
    return unless import_url.present? && valid_import_url?
610

James Lopez's avatar
James Lopez committed
611
    project_import_data = import_data || build_import_data
James Lopez's avatar
James Lopez committed
612
613
614
615
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
616
617
618
619
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
620
  end
621

622
  def import?
623
    external_import? || forked? || gitlab_project_import?
624
625
  end

626
627
628
629
  def no_import?
    import_status == 'none'
  end

630
  def external_import?
631
632
633
    import_url.present?
  end

634
  def imported?
635
636
637
638
    import_finished?
  end

  def import_in_progress?
639
640
641
642
    import_started? || import_scheduled?
  end

  def import_started?
643
644
645
    import? && import_status == 'started'
  end

646
647
648
649
  def import_scheduled?
    import_status == 'scheduled'
  end

650
651
652
653
654
655
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
656
657
  end

658
  def safe_import_url
659
    Gitlab::UrlSanitizer.new(import_url).masked_url
660
661
  end

662
663
664
665
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
666
667
668
669
  def gitea_import?
    import_type == 'gitea'
  end

670
671
672
673
  def github_import?
    import_type == 'github'
  end

674
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
675
    unless creator.can_create_project? || namespace.kind == 'group'
676
677
678
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
679
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
680
      else
Phil Hughes's avatar
Phil Hughes committed
681
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
682
      end
683
684
    end
  rescue
Douwe Maan's avatar
Douwe Maan committed
685
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
686
687
  end

Douwe Maan's avatar
Douwe Maan committed
688
689
690
691
692
693
694
695
696
697
698
699
700
  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.")
701
702
  end

703
704
705
706
707
708
709
710
711
712
  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

713
  def to_param
714
715
716
717
718
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
719
720
  end

721
  # `from` argument can be a Namespace or Project.
722
723
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
724
      full_path
725
726
727
    elsif cross_project_reference?(from)
      path
    end
728
729
730
731
732
733
734
735
  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
736
737
  end

738
  def web_url
739
    Gitlab::Routing.url_helpers.project_url(self)
740
741
  end

742
  def new_issue_address(author)
743
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
744

745
746
747
    author.ensure_incoming_email_token!

    Gitlab::IncomingEmail.reply_address(
748
      "#{full_path}+#{author.incoming_email_token}")
749
750
  end

751
  def build_commit_note(commit)
752
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
753
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
754

755
  def last_activity
756
    last_event
gitlabhq's avatar
gitlabhq committed
757
758
759
  end

  def last_activity_date
760
    last_repository_updated_at || last_activity_at || updated_at
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
761
  end
762

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
763
764
765
  def project_id
    self.id
  end
randx's avatar
randx committed
766

767
  def get_issue(issue_id, current_user)
768
769
770
771
772
    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
773
      ExternalIssue.new(issue_id, self)
774
775
776
    end
  end

Robert Speicher's avatar
Robert Speicher committed
777
  def issue_exists?(issue_id)
778
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
779
780
  end

781
  def default_issue_tracker
782
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
783
784
785
786
787
788
789
790
791
792
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

793
  def external_issue_reference_pattern
794
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
795
796
  end

797
  def default_issues_tracker?
798
    !external_issue_tracker
799
800
801
  end

  def external_issue_tracker
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
    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?)
817
818
  end

819
820
821
822
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
  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

839
  def find_or_initialize_services(exceptions: [])
840
841
    services_templates = Service.where(template: true)

842
843
844
    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
845
      service = find_service(services, service_name)
846

847
848
849
      if service
        service
      else
850
851
852
853
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
854
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
855
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
856
        else
857
          Service.build_from_template(id, template)
858
859
        end
      end
860
861
862
    end
  end

863
864
865
866
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

Valery Sizov's avatar
Valery Sizov committed
867
868
  def create_labels
    Label.templates.each do |label|
869
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
870
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
Valery Sizov's avatar
Valery Sizov committed
871
872
873
    end
  end

874
875
876
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
877

878
  def ci_services
879
    services.where(category: :ci)
880
881
882
  end

  def ci_service
883
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
884
885
  end

886
887
888
889
890
  def deployment_services
    services.where(category: :deployment)
  end

  def deployment_service
891
    @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
892
893
  end

894
895
896
897
898
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
899
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
900
901
  end

Drew Blessing's avatar
Drew Blessing committed
902
903
904
905
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

906
  def avatar_type
907
908
    unless self.avatar.image?
      self.errors.add :avatar, 'only images allowed'
909
910
911
912
    end
  end

  def avatar_in_git
913
    repository.avatar