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 66.3 KB
Newer Older
1
2
# frozen_string_literal: true

3
4
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
5
class Project < ActiveRecord::Base
6
  include Gitlab::ConfigHelper
7
  include Gitlab::ShellAdapter
8
  include Gitlab::VisibilityLevel
Rémy Coutable's avatar
Rémy Coutable committed
9
  include AccessRequestable
10
  include Avatarable
11
  include CacheMarkdownField
12
13
  include Referable
  include Sortable
14
  include AfterCommitQueue
15
  include CaseSensitivity
16
  include TokenAuthenticatable
17
  include ValidAttribute
Felipe Artur's avatar
Felipe Artur committed
18
  include ProjectFeaturesCompatibility
19
  include SelectForProjectAuthorization
20
  include Presentable
21
  include Routable
22
  include GroupDescendant
23
  include Gitlab::SQL::Pattern
24
  include DeploymentPlatform
25
  include ::Gitlab::Utils::StrongMemoize
26
  include ChronicDurationAttribute
27
  include FastDestroyAll::Helpers
Jan Provaznik's avatar
Jan Provaznik committed
28
  include WithUploads
29
  include BatchDestroyDependentAssociations
30
  include FeatureGate
31
  include OptionallySearch
32
  extend Gitlab::Cache::RequestCache
Robert Speicher's avatar
Robert Speicher committed
33

34
  extend Gitlab::ConfigHelper
35

36
  BoardLimitExceeded = Class.new(StandardError)
37

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

51
52
53
  # Valids ports to import from
  VALID_IMPORT_PORTS = [22, 80, 443].freeze

54
55
  cache_markdown_field :description, pipeline: :description

56
57
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
58
                                                       allow_nil: true
Felipe Artur's avatar
Felipe Artur committed
59

60
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
61

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

75
  add_authentication_token_field :runners_token
76

77
  before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
78

79
  before_save :ensure_runners_token
80

81
  after_save :update_project_statistics, if: :namespace_id_changed?
82
83
84

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

85
  after_create :create_project_feature, unless: :project_feature
86

87
  after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
88
  before_destroy :untrack_site_statistics
89

90
91
92
93
  after_create :create_ci_cd_settings,
    unless: :ci_cd_settings,
    if: proc { ProjectCiCdSetting.available? }

94
  after_create :set_last_activity_at
95
  after_create :set_last_repository_updated_at
96
  after_update :update_forks_visibility_level
97

98
  before_destroy :remove_private_deploy_keys
99

100
  use_fast_destroy :build_trace_chunks
101

102
  after_destroy -> { run_after_commit { remove_pages } }
103
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
104

105
106
  after_validation :check_pending_delete

107
  # Storage specific hooks
108
  after_initialize :use_hashed_storage
109
  after_create :check_repository_absence!
110
111
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
112

113
  acts_as_taggable
114

115
  attr_accessor :old_path_with_namespace
116
  attr_accessor :template_name
117
  attr_writer :pipeline_status
118
  attr_accessor :skip_disk_validation
119

120
121
  alias_attribute :title, :name

122
  # Relations
123
  belongs_to :creator, class_name: 'User'
124
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
125
  belongs_to :namespace
126
127
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
128

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

132
  # Project services
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
  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 :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
Matt Coleman's avatar
Matt Coleman committed
163
  has_one :packagist_service
164
  has_one :hangouts_chat_service
165

166
  # TODO: replace these relations with the fork network versions
167
168
  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
  has_one  :forked_from_project,  through:   :forked_project_link
169
170

  has_many :forked_project_links, foreign_key: "forked_from_project_id"
171
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
172
173
174
175
176
177
178
  # 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
179
  has_one :fork_network, through: :fork_network_member
180

181
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
182
  has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
183

184
  # Merge Requests for target project should be removed with it
185
  has_many :merge_requests, foreign_key: 'target_project_id'
186
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
187
188
189
190
191
192
193
194
195
196
  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
197
  has_many :repository_languages, -> { order "share DESC" }
198

199
  has_many :project_authorizations
200
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
201
  has_many :project_members, -> { where(requested_at: nil) },
202
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
203

204
  alias_method :members, :project_members
205
  has_many :users, through: :project_members
206

207
  has_many :requesters, -> { where.not(requested_at: nil) },
208
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
209
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
210

211
  has_many :deploy_keys_projects
212
  has_many :deploy_keys, through: :deploy_keys_projects
213
  has_many :users_star_projects
214
  has_many :starrers, through: :users_star_projects, source: :user
215
  has_many :releases
216
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
217
  has_many :lfs_objects, through: :lfs_objects_projects
218
  has_many :lfs_file_locks
219
  has_many :project_group_links
220
  has_many :invited_groups, through: :project_group_links, source: :group
221
222
  has_many :pages_domains
  has_many :todos
223
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
224

225
226
  has_many :internal_ids

227
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
228
  has_one :project_feature, inverse_of: :project
229
  has_one :statistics, class_name: 'ProjectStatistics'
230

Shinya Maeda's avatar
Shinya Maeda committed
231
  has_one :cluster_project, class_name: 'Clusters::Project'
232
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
233
  has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
234

235
236
237
  # 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.
238
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
239

240
  has_many :commit_statuses
241
  has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
242
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
243
244
245
246
247

  # 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.
248
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
249
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
250
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
251
  has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
252
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
253
  has_many :variables, class_name: 'Ci::Variable'
254
255
256
257
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
Mayra Cabrera's avatar
Mayra Cabrera committed
258
  has_many :project_deploy_tokens
259
  has_many :deploy_tokens, through: :project_deploy_tokens
260

261
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
262
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
263

264
  has_many :project_badges, class_name: 'ProjectBadge'
265
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
266

267
268
  has_many :remote_mirrors, inverse_of: :project

269
  accepts_nested_attributes_for :variables, allow_destroy: true
270
  accepts_nested_attributes_for :project_feature, update_only: true
271
  accepts_nested_attributes_for :import_data
272
  accepts_nested_attributes_for :auto_devops, update_only: true
273

274
275
276
277
  accepts_nested_attributes_for :remote_mirrors,
                                allow_destroy: true,
                                reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }

278
  delegate :name, to: :owner, allow_nil: true, prefix: true
279
  delegate :members, to: :team, prefix: true
280
  delegate :add_user, :add_users, to: :team
281
282
  delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
  delegate :add_master, to: :team # @deprecated
283
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
284

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

303
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
304
  validates :name, uniqueness: { scope: :namespace_id }
305
306
  validates :import_url, url: { protocols: %w(http https ssh git),
                                allow_localhost: false,
307
                                enforce_user: true,
308
                                ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?]
309
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
310
  validate :check_limit, on: :create
311
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
Douwe Maan's avatar
Douwe Maan committed
312
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
313
  validate :visibility_level_allowed_as_fork
314
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
315
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
316
317
318
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
319
  validates :variables, variable_duplicates: { scope: :environment_scope }
320

321
  # Scopes
322
  scope :pending_delete, -> { where(pending_delete: true) }
323
  scope :without_deleted, -> { where(pending_delete: false) }
324

325
326
327
  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) }
328

329
330
  # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
  scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
331
332
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

333
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
334
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
335
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
336
  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
337
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
338
  scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
339
  scope :archived, -> { where(archived: true) }
340
  scope :non_archived, -> { where(archived: false) }
341
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
Rémy Coutable's avatar
Rémy Coutable committed
342
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
343
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
Markus Koller's avatar
Markus Koller committed
344
  scope :with_statistics, -> { includes(:statistics) }
345
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
346
347
348
  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.
349
350
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
351
  end
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366

  # "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) }
367
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
368
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
369
  scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
370

371
372
373
374
375
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

376
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
377

378
  chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
379
380

  validates :build_timeout, allow_nil: true,
381
382
383
384
                            numericality: { greater_than_or_equal_to: 10.minutes,
                                            less_than: 1.month,
                                            only_integer: true,
                                            message: 'needs to be beetween 10 minutes and 1 month' }
385

386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
  # Paginates a collection using a `WHERE id < ?` condition.
  #
  # before - A project ID to use for filtering out projects with an equal or
  #      greater ID. If no ID is given, all projects are included.
  #
  # limit - The maximum number of rows to include.
  def self.paginate_in_descending_order_using_id(
    before: nil,
    limit: Kaminari.config.default_per_page
  )
    relation = order_id_desc.limit(limit)
    relation = relation.where('projects.id < ?', before) if before

    relation
  end

  def self.eager_load_namespace_and_owner
    includes(namespace: :owner)
  end

406
407
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
408
409
  def self.public_or_visible_to_user(user = nil)
    if user
410
411
412
      where('EXISTS (?) OR projects.visibility_level IN (?)',
            user.authorizations_for_projects,
            Gitlab::VisibilityLevel.levels_for_user(user))
413
    else
414
      public_to_user
415
416
417
    end
  end

418
419
420
  # 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.
421
422
423
424
  #
  # 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.
425
  def self.with_feature_available_for_user(feature, user)
426
427
428
429
430
431
432
    visible = [nil, ProjectFeature::ENABLED]

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

433
434
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
435
436
              visible,
              ProjectFeature::PRIVATE,
437
              user.authorizations_for_projects)
438
439
440
    else
      with_feature_access_level(feature, visible)
    end
441
  end
Felipe Artur's avatar
Felipe Artur committed
442

Rémy Coutable's avatar
Rémy Coutable committed
443
444
  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) }
445

446
447
  scope :excluding_project, ->(project) { where.not(id: project) }

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

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
451
  class << self
452
453
454
455
456
457
458
    # 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.
459
    def search(query)
460
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
461
    end
462

463
    def search_by_title(query)
464
      non_archived.fuzzy_search(query, [:name])
465
466
    end

467
468
469
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
470

471
    def sort_by_attribute(method)
472
473
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
474
475
476
        # 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')
477
478
479
480
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
481
482
      else
        order_by(method)
483
484
      end
    end
485
486

    def reference_pattern
487
      %r{
488
489
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
490
      }x
491
    end
492

493
494
495
496
    def reference_postfix
      '>'
    end

497
498
499
500
    def reference_postfix_escaped
      '&gt;'
    end

501
    # Pattern used to extract `namespace/project>` project references from text.
502
503
    # '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
    # when the reference comes from an external source.
504
505
506
    def markdown_reference_pattern
      %r{
        #{reference_pattern}
507
        (#{reference_postfix}|#{reference_postfix_escaped})
508
509
510
      }x
    end

511
    def trending
512
513
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
514
    end
515
516
517
518
519
520

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

    def group_ids
523
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
524
    end
525
526
  end

527
528
529
530
531
532
533
  # 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

534
  def lfs_enabled?
535
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
536

537
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
538
539
  end

540
  def auto_devops_enabled?
541
    if auto_devops&.enabled.nil?
542
      has_auto_devops_implicitly_enabled?
543
544
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
545
    end
546
547
  end

548
  def has_auto_devops_implicitly_enabled?
549
550
    auto_devops&.enabled.nil? &&
      (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
551
552
  end

553
  def has_auto_devops_implicitly_disabled?
554
    auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
555
556
  end

557
558
559
560
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
561
  def team
562
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
563
564
565
  end

  def repository
566
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
567
568
  end

569
570
  def cleanup
    @repository&.cleanup
571
572
573
    @repository = nil
  end

574
575
  alias_method :reload_repository!, :cleanup

Andre Guedes's avatar
Andre Guedes committed
576
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
577
    if Gitlab.config.registry.enabled
578
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
579
    end
580
581
  end

582
  def has_container_registry_tags?
583
584
585
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
586
      has_root_container_repository_tags?
587
588
  end

589
590
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
591
592
  end

593
594
595
596
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

597
598
599
600
  def commits_by(oids:)
    repository.commits_by(oids: oids)
  end

601
  # ref can't be HEAD, can only be branch/tag name or SHA
602
  def latest_successful_builds_for(ref = default_branch)
603
    latest_pipeline = pipelines.latest_successful_for(ref)
604
605

    if latest_pipeline
606
      latest_pipeline.builds.latest.with_artifacts_archive
607
608
609
    else
      builds.none
    end
610
611
  end

612
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
613
    sha = repository.merge_base(first_commit_id, second_commit_id)
614
    commit_by(oid: sha) if sha
615
616
  end

617
  def saved?
618
    id && persisted?
619
620
  end

621
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
622
623
    job_id =
      if forked?
624
        RepositoryForkWorker.perform_async(id)
625
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
626
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
627
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
628
629
630
      else
        RepositoryImportWorker.perform_async(self.id)
      end
631

632
633
634
635
636
637
638
639
    log_import_activity(job_id)

    job_id
  end

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

640
    if job_id
641
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
642
    else
643
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
644
    end
645
646
  end

647
648
649
650
651
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

652
    update(import_error: nil)
653
654
655
656
657
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
658
    import_data&.destroy
659
660
  end

661
  def ci_config_path=(value)
662
    # Strip all leading slashes so that //foo -> foo
663
    super(value&.delete("\0"))
664
665
  end

666
  def import_url=(value)
James Lopez's avatar
James Lopez committed
667
668
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

669
    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
670
    super(import_url.sanitized_url)
671
    create_or_update_import_data(credentials: import_url.credentials)
672
673
674
  end

  def import_url
James Lopez's avatar
James Lopez committed
675
    if import_data && super.present?
676
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
677
678
679
      import_url.full_url
    else
      super
680
681
    end
  end
682

James Lopez's avatar
James Lopez committed
683
  def valid_import_url?
684
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
685
686
  end

687
  def create_or_update_import_data(data: nil, credentials: nil)
688
    return if data.nil? && credentials.nil?
689

James Lopez's avatar
James Lopez committed
690
    project_import_data = import_data || build_import_data
James Lopez's avatar
James Lopez committed
691
692
693
694
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
695

696
697
698
699
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
700
701

    project_import_data
702
  end
703

704
  def import?
James Lopez's avatar
James Lopez committed
705
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
706
707
708
  end

  def external_import?
709
710
711
    import_url.present?
  end

712
  def imported?
713
714
715
716
    import_finished?
  end

  def import_in_progress?
717
718
719
    import_started? || import_scheduled?
  end

720
721
722
723
724
725
726
727
  def import_state_args
    {
      status: self[:import_status],
      jid: self[:import_jid],
      last_error: self[:import_error]
    }
  end

728
729
  def ensure_import_state(force: false)
    return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
730
731
    return unless import_state.nil?

732
733
    if persisted?
      create_import_state(import_state_args)
734

735
736
737
738
739
740
      update_column(:import_status, 'none')
    else
      build_import_state(import_state_args)

      self[:import_status] = 'none'
    end
741
742
  end

743
744
745
746
747
748
  def human_import_status_name
    ensure_import_state

    import_state.human_status_name
  end

749
  def import_schedule
750
    ensure_import_state(force: true)
751

752
    import_state.schedule
753
754
755
  end

  def force_import_start
756
    ensure_import_state(force: true)
757

758
    import_state.force_start
759
760
761
  end

  def import_start
762
    ensure_import_state(force: true)
763

764
    import_state.start
765
766
767
  end

  def import_fail
768
    ensure_import_state(force: true)
769

770
    import_state.fail_op
771
772
773
  end

  def import_finish
774
    ensure_import_state(force: true)
775

776
    import_state.finish
777
778
779
  end

  def import_jid=(new_jid)
780
    ensure_import_state(force: true)
781

782
    import_state.jid = new_jid
783
784
785
786
787
788
789
790
791
  end

  def import_jid
    ensure_import_state

    import_state&.jid
  end

  def import_error=(new_error)
792
    ensure_import_state(force: true)
793

794
    import_state.last_error = new_error
795
796
797
798
799
800
801
802
803
  end

  def import_error
    ensure_import_state

    import_state&.last_error
  end

  def import_status=(new_status)
804
    ensure_import_state(force: true)
805

806
    import_state.status = new_status
807
808
809
810
811
812
813
814
815
816
817
818
  end

  def import_status
    ensure_import_state

    import_state&.status || 'none'
  end

  def no_import?
    import_status == 'none'
  end

819
  def import_started?
820
821
    # import? does SQL work so only run it if it looks like there's an import running
    import_status == 'started' && import?
822
823
  end

824
825
826
827
  def import_scheduled?
    import_status == 'scheduled'
  end

828
829
830
831
832
833
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
834
835
  end

836
  def safe_import_url
837
    Gitlab::UrlSanitizer.new(import_url).masked_url
838
839
  end

840
841
842
843
  def bare_repository_import?
    import_type == 'bare_repository'
  end

844
845
846
847
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
848
849
850
851
  def gitea_import?
    import_type == 'gitea'
  end

852
853
854
855
856
857
858
859
860
861
862
863
864