project.rb 66.5 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
  include FromUnion
33
  extend Gitlab::Cache::RequestCache
Robert Speicher's avatar
Robert Speicher committed
34

35
  extend Gitlab::ConfigHelper
36

37
  BoardLimitExceeded = Class.new(StandardError)
38

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

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

55
56
  cache_markdown_field :description, pipeline: :description

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

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

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

76
  add_authentication_token_field :runners_token
77

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

80
  before_save :ensure_runners_token
81

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

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

86
  after_create :create_project_feature, unless: :project_feature
87

88
  after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) }
89
  before_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) }
90

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

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

99
  before_destroy :remove_private_deploy_keys
100

101
  use_fast_destroy :build_trace_chunks
102

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

106
107
  after_validation :check_pending_delete

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

114
  acts_as_ordered_taggable
115

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

121
122
  alias_attribute :title, :name

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

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

133
  # Project services
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
163
  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
164
  has_one :packagist_service
165
  has_one :hangouts_chat_service
166

167
168
169
170
171
  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
172
  has_one :fork_network, through: :fork_network_member
173
174
175
  has_one :forked_from_project, through: :fork_network_member
  has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
  has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
176

177
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
178
  has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
179

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

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

200
  alias_method :members, :project_members
201
  has_many :users, through: :project_members
202

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

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

221
222
  has_many :internal_ids

223
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
224
  has_one :project_feature, inverse_of: :project
225
  has_one :statistics, class_name: 'ProjectStatistics'
226

Shinya Maeda's avatar
Shinya Maeda committed
227
  has_one :cluster_project, class_name: 'Clusters::Project'
228
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
229
  has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
230

231
232
  has_many :prometheus_metrics

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

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

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

259
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
260
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
261

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

265
266
  has_many :remote_mirrors, inverse_of: :project

267
  accepts_nested_attributes_for :variables, allow_destroy: true
268
  accepts_nested_attributes_for :project_feature, update_only: true
269
  accepts_nested_attributes_for :import_data
270
  accepts_nested_attributes_for :auto_devops, update_only: true
271

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

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

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

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

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

323
324
325
  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) }
326

327
328
  # 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") }
329
  scope :sorted_by_stars, -> { reorder(star_count: :desc) }
330

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

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
    access_level_attribute = ProjectFeature.access_level_attribute(feature)
354
    with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] })
355
356
357
358
359
360
361
362
363
364
  }

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

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

374
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
375

376
  chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
377
378

  validates :build_timeout, allow_nil: true,
379
380
381
382
                            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' }
383

384
385
386
387
388
389
390
  # Returns a project, if it is not about to be removed.
  #
  # id - The ID of the project to retrieve.
  def self.find_without_deleted(id)
    without_deleted.find_by_id(id)
  end

391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
  # 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

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

423
  # project features may be "disabled", "internal", "enabled" or "public". If "internal",
424
  # they are only available to team members. This scope returns projects where
425
  # the feature is either public, enabled, or internal with permission for the user.
426
427
428
429
  #
  # 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.
430
  def self.with_feature_available_for_user(feature, user)
431
    visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
432
433
434
435
436
437

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

438
439
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
440
441
              visible,
              ProjectFeature::PRIVATE,
442
              user.authorizations_for_projects)
443
444
445
    else
      with_feature_access_level(feature, visible)
    end
446
  end
Felipe Artur's avatar
Felipe Artur committed
447

Rémy Coutable's avatar
Rémy Coutable committed
448
449
  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) }
450

451
452
  scope :excluding_project, ->(project) { where.not(id: project) }

453
454
  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'") }
455
  scope :for_group, -> (group) { where(group: group) }
456

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
457
  class << self
458
459
460
461
462
463
464
    # 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.
465
    def search(query)
466
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
467
    end
468

469
    def search_by_title(query)
470
      non_archived.fuzzy_search(query, [:name])
471
472
    end

473
474
475
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
476

477
    def sort_by_attribute(method)
478
479
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
480
481
482
        # 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')
483
484
485
486
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
487
488
      when 'stars_desc'
        sorted_by_stars
489
490
      else
        order_by(method)
491
492
      end
    end
493
494

    def reference_pattern
495
      %r{
496
497
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
498
      }x
499
    end
500

501
502
503
504
    def reference_postfix
      '>'
    end

505
506
507
508
    def reference_postfix_escaped
      '&gt;'
    end

509
    # Pattern used to extract `namespace/project>` project references from text.
510
511
    # '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
    # when the reference comes from an external source.
512
513
514
    def markdown_reference_pattern
      %r{
        #{reference_pattern}
515
        (#{reference_postfix}|#{reference_postfix_escaped})
516
517
518
      }x
    end

519
    def trending
520
521
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
522
    end
523
524
525
526
527
528

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

    def group_ids
531
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
532
    end
533
534
  end

535
536
537
538
539
540
541
  # 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

542
  def lfs_enabled?
543
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
544

545
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
546
547
  end

548
  def auto_devops_enabled?
549
    if auto_devops&.enabled.nil?
550
      has_auto_devops_implicitly_enabled?
551
552
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
553
    end
554
555
  end

556
  def has_auto_devops_implicitly_enabled?
557
558
    auto_devops&.enabled.nil? &&
      (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
559
560
  end

561
  def has_auto_devops_implicitly_disabled?
562
    auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
563
564
  end

565
566
567
568
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
569
  def team
570
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
571
572
573
  end

  def repository
574
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
575
576
  end

577
  def cleanup
578
579
580
    @repository = nil
  end

581
582
  alias_method :reload_repository!, :cleanup

Andre Guedes's avatar
Andre Guedes committed
583
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
584
    if Gitlab.config.registry.enabled
585
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
586
    end
587
588
  end

589
  def has_container_registry_tags?
590
591
592
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
593
      has_root_container_repository_tags?
594
595
  end

596
597
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
598
599
  end

600
601
602
603
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

604
605
606
607
  def commits_by(oids:)
    repository.commits_by(oids: oids)
  end

608
  # ref can't be HEAD, can only be branch/tag name or SHA
609
  def latest_successful_builds_for(ref = default_branch)
610
    latest_pipeline = pipelines.latest_successful_for(ref)
611
612

    if latest_pipeline
613
      latest_pipeline.builds.latest.with_artifacts_archive
614
615
616
    else
      builds.none
    end
617
618
  end

619
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
620
    sha = repository.merge_base(first_commit_id, second_commit_id)
621
    commit_by(oid: sha) if sha
622
623
  end

624
  def saved?
625
    id && persisted?
626
627
  end

628
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
629
630
    job_id =
      if forked?
631
        RepositoryForkWorker.perform_async(id)
632
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
633
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
634
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
635
636
637
      else
        RepositoryImportWorker.perform_async(self.id)
      end
638

639
640
641
642
643
644
645
646
    log_import_activity(job_id)

    job_id
  end

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

647
    if job_id
648
      Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
649
    else
650
      Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
651
    end
652
653
  end

654
655
656
657
658
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

659
    update(import_error: nil)
660
661
662
663
664
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
665
    import_data&.destroy
666
667
  end

668
  def ci_config_path=(value)
669
    # Strip all leading slashes so that //foo -> foo
670
    super(value&.delete("\0"))
671
672
  end

673
  def import_url=(value)
James Lopez's avatar
James Lopez committed
674
675
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

676
    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
677
    super(import_url.sanitized_url)
678
    create_or_update_import_data(credentials: import_url.credentials)
679
680
681
  end

  def import_url
James Lopez's avatar
James Lopez committed
682
    if import_data && super.present?
683
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
684
685
686
      import_url.full_url
    else
      super
687
688
    end
  end
689

James Lopez's avatar
James Lopez committed
690
  def valid_import_url?
691
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
692
693
  end

694
  def create_or_update_import_data(data: nil, credentials: nil)
695
    return if data.nil? && credentials.nil?
696

James Lopez's avatar
James Lopez committed
697
    project_import_data = import_data || build_import_data
James Lopez's avatar
James Lopez committed
698
699
700
701
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
702

703
704
705
706
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
707
708

    project_import_data
709
  end
710

711
  def import?
James Lopez's avatar
James Lopez committed
712
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
713
714
715
  end

  def external_import?
716
717
718
    import_url.present?
  end

719
  def imported?
720
721
722
723
    import_finished?
  end

  def import_in_progress?
724
725
726
    import_started? || import_scheduled?
  end

727
728
729
730
731
732
733
734
  def import_state_args
    {
      status: self[:import_status],
      jid: self[:import_jid],
      last_error: self[:import_error]
    }
  end

735
736
  def ensure_import_state(force: false)
    return if !force && (self[:import_status] == 'none' || self[:import_status].nil?)
737
738
    return unless import_state.nil?

739
740
    if persisted?
      create_import_state(import_state_args)
741

742
743
744
745
746
747
      update_column(:import_status, 'none')
    else
      build_import_state(import_state_args)

      self[:import_status] = 'none'
    end
748
749
  end

750
751
752
753
754
755
  def human_import_status_name
    ensure_import_state

    import_state.human_status_name
  end

756
  def import_schedule
757
    ensure_import_state(force: true)
758

759
    import_state.schedule
760
761
762
  end

  def force_import_start
763
    ensure_import_state(force: true)
764

765
    import_state.force_start
766
767
768
  end

  def import_start
769
    ensure_import_state(force: true)
770

771
    import_state.start
772
773
774
  end

  def import_fail
775
    ensure_import_state(force: true)
776

777
    import_state.fail_op
778
779
780
  end

  def import_finish
781
    ensure_import_state(force: true)
782

783
    import_state.finish
784
785
786
  end

  def import_jid=(new_jid)
787
    ensure_import_state(force: true)
788

789
    import_state.jid = new_jid
790
791
792
793
794
795
796
797
798
  end

  def import_jid
    ensure_import_state

    import_state&.jid
  end

  def import_error=(new_error)