project.rb 86.3 KB
Newer Older
1
2
# frozen_string_literal: true

3
4
require 'carrierwave/orm/activerecord'

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

39
  extend Gitlab::ConfigHelper
40

41
  BoardLimitExceeded = Class.new(StandardError)
42

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

55
56
57
58
59
  VALID_IMPORT_PORTS = [80, 443].freeze
  VALID_IMPORT_PROTOCOLS = %w(http https git).freeze

  VALID_MIRROR_PORTS = [22, 80, 443].freeze
  VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
60

61
62
  ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10

George Koltsov's avatar
George Koltsov committed
63
  SORTING_PREFERENCE_FIELD = :projects_sort
64
  MAX_BUILD_TIMEOUT = 1.month
George Koltsov's avatar
George Koltsov committed
65

66
67
  cache_markdown_field :description, pipeline: :description

68
  default_value_for :packages_enabled, true
69
  default_value_for :archived, false
70
  default_value_for :resolve_outdated_diff_discussions, false
71
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
72
  default_value_for(:repository_storage) do
73
    pick_repository_storage
74
75
  end

76
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
77
78
79
80
81
  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
82
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
83
  default_value_for :remove_source_branch_after_merge, true
84
  default_value_for :autoclose_referenced_issues, true
85
  default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
86

87
  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
88

89
  before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
90

91
  before_save :ensure_runners_token
92

Jasper Maes's avatar
Jasper Maes committed
93
  after_save :update_project_statistics, if: :saved_change_to_namespace_id?
94
95
96

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

97
  after_create :create_project_feature, unless: :project_feature
98
99

  after_create :create_ci_cd_settings,
100
    unless: :ci_cd_settings
101

102
103
104
  after_create :create_container_expiration_policy,
               unless: :container_expiration_policy

105
  after_create :create_pages_metadatum,
106
               unless: :pages_metadatum
107

Mark Chao's avatar
Mark Chao committed
108
  after_create :set_timestamps_for_create
109
  after_update :update_forks_visibility_level
110

111
  before_destroy :remove_private_deploy_keys
112

113
  use_fast_destroy :build_trace_chunks
114

115
  after_destroy -> { run_after_commit { remove_pages } }
116
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
117

118
119
  after_validation :check_pending_delete

120
  # Storage specific hooks
121
  after_initialize :use_hashed_storage
122
  after_create :check_repository_absence!
123

124
  acts_as_ordered_taggable
125

126
  attr_accessor :old_path_with_namespace
127
  attr_accessor :template_name
128
  attr_writer :pipeline_status
129
  attr_accessor :skip_disk_validation
130

131
132
  alias_attribute :title, :name

133
  # Relations
Nick Thomas's avatar
Nick Thomas committed
134
  belongs_to :pool_repository
135
  belongs_to :creator, class_name: 'User'
136
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
137
  belongs_to :namespace
138
139
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
140

141
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
142
  has_many :boards
Felipe Artur's avatar
Felipe Artur committed
143

144
  # Project services
145
  has_one :alerts_service
146
  has_one :campfire_service
blackst0ne's avatar
blackst0ne committed
147
  has_one :discord_service
148
149
150
151
152
  has_one :drone_ci_service
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
153
  has_one :hipchat_service
154
155
156
157
158
159
160
161
162
163
164
165
166
  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
Yauhen Kotau's avatar
Yauhen Kotau committed
167
  has_one :youtrack_service
168
169
  has_one :custom_issue_tracker_service
  has_one :bugzilla_service
170
  has_one :confluence_service
171
172
173
174
175
176
  has_one :external_wiki_service
  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
177
  has_one :packagist_service
178
  has_one :hangouts_chat_service
179
  has_one :unify_circuit_service
180
  has_one :webex_teams_service
181

182
183
184
185
186
  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
187
  has_one :fork_network, through: :fork_network_member
188
189
190
  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
191
  has_many :fork_network_projects, through: :fork_network, source: :projects
192

193
194
195
196
  # Packages
  has_many :packages, class_name: 'Packages::Package'
  has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'

197
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
198
  has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
199
  has_many :export_jobs, class_name: 'ProjectExportJob'
200
  has_one :project_repository, inverse_of: :project
201
  has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
202
  has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
203
  has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
204
  has_one :grafana_integration, inverse_of: :project
205
  has_one :project_setting, inverse_of: :project, autosave: true
206
  has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
207
  has_one :service_desk_setting, class_name: 'ServiceDeskSetting'
208

209
  # Merge Requests for target project should be removed with it
210
  has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
211
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
212
213
214
215
216
  has_many :issues
  has_many :labels, class_name: 'ProjectLabel'
  has_many :services
  has_many :events
  has_many :milestones
217
  has_many :iterations
218
219
220
221
222
  has_many :notes
  has_many :snippets, class_name: 'ProjectSnippet'
  has_many :hooks, class_name: 'ProjectHook'
  has_many :protected_branches
  has_many :protected_tags
223
  has_many :repository_languages, -> { order "share DESC" }
224
  has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
225

226
  has_many :project_authorizations
227
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
228
  has_many :project_members, -> { where(requested_at: nil) },
229
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
230

231
  alias_method :members, :project_members
232
  has_many :users, through: :project_members
233

234
  has_many :requesters, -> { where.not(requested_at: nil) },
235
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
236
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
237

238
  has_many :deploy_keys_projects, inverse_of: :project
239
  has_many :deploy_keys, through: :deploy_keys_projects
240
  has_many :users_star_projects
241
  has_many :starrers, through: :users_star_projects, source: :user
242
  has_many :releases
243
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
244
  has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects
245
  has_many :lfs_file_locks
246
  has_many :project_group_links
247
  has_many :invited_groups, through: :project_group_links, source: :group
248
249
  has_many :pages_domains
  has_many :todos
250
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
251

252
253
  has_many :internal_ids

254
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
255
  has_one :project_feature, inverse_of: :project
256
  has_one :statistics, class_name: 'ProjectStatistics'
257

Shinya Maeda's avatar
Shinya Maeda committed
258
  has_one :cluster_project, class_name: 'Clusters::Project'
259
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
260
  has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
261
  has_many :management_clusters, class_name: 'Clusters::Cluster', foreign_key: :management_project_id, inverse_of: :management_project
262
  has_many :cluster_agents, class_name: 'Clusters::Agent'
263

264
  has_many :prometheus_metrics
265
  has_many :prometheus_alerts, inverse_of: :project
266
267
  has_many :prometheus_alert_events, inverse_of: :project
  has_many :self_managed_prometheus_alert_events, inverse_of: :project
268
269
270
  has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project

  has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
271

272
273
274
  # 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.
275
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
276
  has_one :container_expiration_policy, inverse_of: :project
277

278
  has_many :commit_statuses
279
  # The relation :all_pipelines is intended to be used when we want to get the
280
281
  # whole list of pipelines associated to the project
  has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
282
  # The relation :ci_pipelines is intended to be used when we want to get only
283
284
285
286
  # those pipeline which are directly related to CI. There are
  # other pipelines, like webide ones, that we won't retrieve
  # if we use this relation.
  has_many :ci_pipelines,
287
          -> { ci_sources },
288
289
          class_name: 'Ci::Pipeline',
          inverse_of: :project
290
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
291
  has_many :ci_refs, class_name: 'Ci::Ref', inverse_of: :project
292
293
294
295
296

  # 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.
297
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
298
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
299
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
300
  has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
301
  has_many :job_artifacts, class_name: 'Ci::JobArtifact'
302
  has_many :pipeline_artifacts, class_name: 'Ci::PipelineArtifact', inverse_of: :project
303
  has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
304
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
305
  has_many :variables, class_name: 'Ci::Variable'
306
307
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
308
  has_many :environments_for_dashboard, -> { from(with_rank.unfoldered.available, :environments).where('rank <= 3') }, class_name: 'Environment'
309
  has_many :deployments
310
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
Mayra Cabrera's avatar
Mayra Cabrera committed
311
  has_many :project_deploy_tokens
312
  has_many :deploy_tokens, through: :project_deploy_tokens
313
  has_many :resource_groups, class_name: 'Ci::ResourceGroup', inverse_of: :project
314
  has_many :freeze_periods, class_name: 'Ci::FreezePeriod', inverse_of: :project
315

Nick Thomas's avatar
Nick Thomas committed
316
  has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true
317
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
318

319
  has_many :project_badges, class_name: 'ProjectBadge'
320
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
321

322
  has_many :remote_mirrors, inverse_of: :project
323
  has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
324

325
326
  has_many :external_pull_requests, inverse_of: :project

327
328
329
  has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
  has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id

330
331
  has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project

332
  has_many :import_failures, inverse_of: :project
333
  has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
334

335
336
337
  has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'

  has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove'
338

339
340
341
  has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
  has_many :reviews, inverse_of: :project

342
343
344
345
  # Can be too many records. We need to implement delete_all in batches.
  # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
  has_many :product_analytics_events, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent

346
  accepts_nested_attributes_for :variables, allow_destroy: true
347
  accepts_nested_attributes_for :project_feature, update_only: true
348
  accepts_nested_attributes_for :project_setting, update_only: true
349
  accepts_nested_attributes_for :import_data
350
  accepts_nested_attributes_for :auto_devops, update_only: true
351
  accepts_nested_attributes_for :ci_cd_settings, update_only: true
352
  accepts_nested_attributes_for :container_expiration_policy, update_only: true
353

354
355
356
357
  accepts_nested_attributes_for :remote_mirrors,
                                allow_destroy: true,
                                reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }

358
  accepts_nested_attributes_for :incident_management_setting, update_only: true
359
  accepts_nested_attributes_for :error_tracking_setting, update_only: true
360
  accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
361
  accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true
362
  accepts_nested_attributes_for :prometheus_service, update_only: true
363
  accepts_nested_attributes_for :alerting_setting, update_only: true
364

365
366
367
368
369
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
    :merge_requests_enabled?, :forking_enabled?, :issues_enabled?,
    :pages_enabled?, :public_pages?, :private_pages?,
    :merge_requests_access_level, :forking_access_level, :issues_access_level,
    :wiki_access_level, :snippets_access_level, :builds_access_level,
370
    :repository_access_level, :pages_access_level, :metrics_dashboard_access_level,
371
    to: :project_feature, allow_nil: true
372
373
374
  delegate :show_default_award_emojis, :show_default_award_emojis=,
    :show_default_award_emojis?,
    to: :project_setting, allow_nil: true
375
376
  delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
    prefix: :import, to: :import_state, allow_nil: true
377
  delegate :squash_always?, :squash_never?, :squash_enabled_by_default?, :squash_readonly?, to: :project_setting
378
  delegate :no_import?, to: :import_state, allow_nil: true
379
  delegate :name, to: :owner, allow_nil: true, prefix: true
380
  delegate :members, to: :team, prefix: true
381
  delegate :add_user, :add_users, to: :team
382
  delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
383
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
384
  delegate :root_ancestor, to: :namespace, allow_nil: true
385
  delegate :last_pipeline, to: :commit, allow_nil: true
386
  delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
387
  delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
388
  delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
389
  delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings
390
  delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
391
392
393
394
  delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?,
    :allow_merge_on_skipped_pipeline=, :has_confluence?,
    to: :project_setting
  delegate :active?, to: :prometheus_service, allow_nil: true, prefix: true
395

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
396
  # Validations
397
  validates :creator, presence: true, on: :create
398
  validates :description, length: { maximum: 2000 }, allow_blank: true
399
  validates :ci_config_path,
400
    format: { without: %r{(\.{2}|\A/)},
401
              message: _('cannot include leading slash or directory traversal.') },
402
403
    length: { maximum: 255 },
    allow_blank: true
404
405
  validates :name,
    presence: true,
406
    length: { maximum: 255 },
407
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
408
              message: Gitlab::Regex.project_name_regex_message }
409
410
  validates :path,
    presence: true,
411
    project_path: true,
412
    length: { maximum: 255 }
413

414
415
  validates :project_feature, presence: true

416
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
417
  validates :name, uniqueness: { scope: :namespace_id }
418
  validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
419
420
                                       ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
                                       enforce_user: true }, if: [:external_import?, :import_url_changed?]
421
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
422
  validate :check_personal_projects_limit, on: :create
423
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
424
425
  validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
  validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
Rob Watson's avatar
Rob Watson committed
426
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
427
428
429
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
430
  validates :variables, variable_duplicates: { scope: :environment_scope }
431
  validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
432
  validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
433

434
  # Scopes
435
  scope :pending_delete, -> { where(pending_delete: true) }
436
  scope :without_deleted, -> { where(pending_delete: false) }
437

438
439
440
441
442
443
444
445
446
447
448
  scope :with_storage_feature, ->(feature) do
    where(arel_table[:storage_version].gteq(HASHED_STORAGE_FEATURES[feature]))
  end
  scope :without_storage_feature, ->(feature) do
    where(arel_table[:storage_version].lt(HASHED_STORAGE_FEATURES[feature])
        .or(arel_table[:storage_version].eq(nil)))
  end
  scope :with_unmigrated_storage, -> do
    where(arel_table[:storage_version].lt(LATEST_STORAGE_VERSION)
        .or(arel_table[:storage_version].eq(nil)))
  end
449

450
  # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
451
  scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) }
452
453
  scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
  scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
454
  # Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
455
  scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
456

457
458
459
460
461
462
463
464
465
466
  scope :sorted_by_similarity_desc, -> (search) do
    order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
      { column: arel_table["path"], multiplier: 1 },
      { column: arel_table["name"], multiplier: 0.7 },
      { column: arel_table["description"], multiplier: 0.2 }
    ])

    reorder(order_expression.desc, arel_table['id'].desc)
  end

467
  scope :with_packages, -> { joins(:packages) }
468
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
469
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
470
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
471
  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
472
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
473
  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)) }
474
  scope :archived, -> { where(archived: true) }
475
  scope :non_archived, -> { where(archived: false) }
476
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
477
  scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
478
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
479
  scope :inc_routes, -> { includes(:route, namespace: :route) }
Markus Koller's avatar
Markus Koller committed
480
  scope :with_statistics, -> { includes(:statistics) }
481
482
  scope :with_namespace, -> { includes(:namespace) }
  scope :with_import_state, -> { includes(:import_state) }
483
  scope :include_project_feature, -> { includes(:project_feature) }
484
  scope :with_service, ->(service) { joins(service).eager_load(service) }
485
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
486
  scope :with_container_registry, -> { where(container_registry_enabled: true) }
487
488
489
  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.
490
491
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
492
  end
493
494
495

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
496
497
498
499
    access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
    enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))

    with_project_feature.where(enabled_feature)
500
501
502
503
504
505
506
507
  }

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

508
509
510
511
512
513
514
515
516
517
  # Picks projects which use the given programming language
  scope :with_programming_language, ->(language_name) do
    lang_id_query = ProgrammingLanguage
        .with_name_case_insensitive(language_name)
        .select(:id)

    joins(:repository_languages)
        .where(repository_languages: { programming_language_id: lang_id_query })
  end

518
  scope :service_desk_enabled, -> { where(service_desk_enabled: true) }
519
520
  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
521
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
522
  scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
523
524
525
  scope :with_issues_or_mrs_available_for_user, -> (user) do
    with_issues_available_for_user(user).or(with_merge_requests_available_for_user(user))
  end
526
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
527
  scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
528
  scope :with_limit, -> (maximum) { limit(maximum) }
529

530
531
532
533
534
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

535
536
537
538
  scope :with_pages_deployed, -> do
    joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
  end

539
540
541
542
543
  scope :pages_metadata_not_migrated, -> do
    left_outer_joins(:pages_metadatum)
      .where(project_pages_metadata: { project_id: nil })
  end

544
545
  scope :with_api_commit_entity_associations, -> {
    preload(:project_feature, :route, namespace: [:route, :owner])
546
547
  }

548
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
549

550
  chronic_duration_attr :build_timeout_human_readable, :build_timeout,
551
    default: 3600, error_message: _('Maximum job timeout has a value which could not be accepted')
552
553

  validates :build_timeout, allow_nil: true,
554
                            numericality: { greater_than_or_equal_to: 10.minutes,
555
                                            less_than: MAX_BUILD_TIMEOUT,
556
                                            only_integer: true,
557
                                            message: _('needs to be between 10 minutes and 1 month') }
558

559
560
561
  # Used by Projects::CleanupService to hold a map of rewritten object IDs
  mount_uploader :bfg_object_map, AttachmentUploader

562
563
564
565
  def self.with_api_entity_associations
    preload(:project_feature, :route, :tags, :group, namespace: [:route, :owner])
  end

566
567
568
569
  def self.with_web_entity_associations
    preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner])
  end

570
571
572
573
  def self.eager_load_namespace_and_owner
    includes(namespace: :owner)
  end

574
575
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
576
577
578
  def self.public_or_visible_to_user(user = nil, min_access_level = nil)
    min_access_level = nil if user&.admin?

579
580
581
582
583
    return public_to_user unless user

    if user.is_a?(DeployToken)
      user.projects
    else
584
      where('EXISTS (?) OR projects.visibility_level IN (?)',
585
            user.authorizations_for_projects(min_access_level: min_access_level),
586
            Gitlab::VisibilityLevel.levels_for_user(user))
587
588
589
    end
  end

590
  # project features may be "disabled", "internal", "enabled" or "public". If "internal",
591
  # they are only available to team members. This scope returns projects where
592
  # the feature is either public, enabled, or internal with permission for the user.
593
594
  # Note: this scope doesn't enforce that the user has access to the projects, it just checks
  # that the user has access to the feature. It's important to use this scope with others
595
  # that checks project authorizations first (e.g. `filter_by_feature_visibility`).
596
597
598
599
  #
  # 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.
600
  def self.with_feature_available_for_user(feature, user)
601
    visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
602
603
604
605

    if user&.admin?
      with_feature_enabled(feature)
    elsif user
606
      min_access_level = ProjectFeature.required_minimum_access_level(feature)
607
608
      column = ProjectFeature.quoted_access_level_column(feature)

609
      with_project_feature
610
611
612
613
614
615
      .where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
            {
              public_visible: visible,
              private_visible: ProjectFeature::PRIVATE,
              authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
            })
616
    else
617
618
      # This has to be added to include features whose value is nil in the db
      visible << nil
619
620
      with_feature_access_level(feature, visible)
    end
621
  end
Felipe Artur's avatar
Felipe Artur committed
622

623
624
625
626
627
628
629
630
  def self.projects_user_can(projects, user, action)
    projects = where(id: projects)

    DeclarativePolicy.user_scope do
      projects.select { |project| Ability.allowed?(user, action, project) }
    end
  end

631
632
  # This scope returns projects where user has access to both the project and the feature.
  def self.filter_by_feature_visibility(feature, user)
633
634
635
636
637
    with_feature_available_for_user(feature, user)
      .public_or_visible_to_user(
        user,
        ProjectFeature.required_minimum_access_level_for_private_project(feature)
      )
638
639
  end

640
641
  def self.wrap_with_cte(collection)
    cte = Gitlab::SQL::CTE.new(:projects_cte, collection)
642
643
644
    Project.with(cte.to_arel).from(cte.alias_to(Project.arel_table))
  end

Rémy Coutable's avatar
Rémy Coutable committed
645
646
  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) }
647

648
649
  scope :excluding_project, ->(project) { where.not(id: project) }

650
651
  # We require an alias to the project_mirror_data_table in order to use import_state in our queries
  scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
652
  scope :for_group, -> (group) { where(group: group) }
653
  scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) }
654
655
  scope :for_repository_storage, -> (repository_storage) { where(repository_storage: repository_storage) }
  scope :excluding_repository_storage, -> (repository_storage) { where.not(repository_storage: repository_storage) }
656

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
657
  class << self
658
659
660
    # Searches for a list of projects based on the query given in `query`.
    #
    # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
661
    # search.
662
663
    #
    # query - The search query as a String.
664
    def search(query, include_namespace: false)
665
      if include_namespace
666
        joins(:route).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name], :description])
667
668
669
      else
        fuzzy_search(query, [:path, :name, :description])
      end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
670
    end
671

672
    def search_by_title(query)
673
      non_archived.fuzzy_search(query, [:name])
674
675
    end

676
677
678
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
679

680
    def sort_by_attribute(method)
681
682
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
683
684
685
        # 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')
686
      when 'latest_activity_desc'
687
        reorder(self.arel_table['last_activity_at'].desc)
688
      when 'latest_activity_asc'
689
        reorder(self.arel_table['last_activity_at'].asc)
690
      when 'stars_desc'
691
        sorted_by_stars_desc
692
693
      when 'stars_asc'
        sorted_by_stars_asc
694
695
      else
        order_by(method)
696
697
      end
    end
698
699

    def reference_pattern
700
      %r{
701
        (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
702
703
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
704
      }x
705
    end
706

707
708
709
710
    def reference_postfix
      '>'
    end

711
712
713
714
    def reference_postfix_escaped
      '&gt;'
    end

715
    # Pattern used to extract `namespace/project>` project references from text.
716
717
    # '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
    # when the reference comes from an external source.
718
    def markdown_reference_pattern
719
720
721
722
723
      @markdown_reference_pattern ||=
        %r{
          #{reference_pattern}
          (#{reference_postfix}|#{reference_postfix_escaped})
        }x
724
725
    end

726
    def trending
727
728
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
729
    end
730
731
732
733
734
735

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

    def group_ids
738
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
739
    end
740

741
    # Returns ids of projects with issuables available for given user
742
    #
743
744
745
    # Used on queries to find milestones or labels which user can see
    # For example: Milestone.where(project_id: ids_with_issuables_available_for(user))
    def ids_with_issuables_available_for(user)
746
747
748
749
750
      with_issues_enabled = with_issues_available_for_user(user).select(:id)
      with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)

      from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
    end
751
752
753
754
755
756

    def find_by_service_desk_project_key(key)
      # project_key is not indexed for now
      # see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24063#note_282435524 for details
      joins(:service_desk_setting).find_by('service_desk_settings.project_key' => key)
    end
757
758
  end

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
759
  def initialize(attributes = nil)
760
761
762
763
764
765
766
767
768
    # We can't use default_value_for because the database has a default
    # value of 0 for visibility_level. If someone attempts to create a
    # private project, default_value_for will assume that the
    # visibility_level hasn't changed and will use the application
    # setting default, which could be internal or public. For projects
    # inside a private group, those levels are invalid.
    #
    # To fix the problem, we assign the actual default in the application if
    # no explicit visibility has been initialized.
Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
769
770
    attributes ||= {}

771
772
773
774
775
776
777
    unless visibility_attribute_present?(attributes)
      attributes[:visibility_level] = Gitlab::CurrentSettings.default_project_visibility
    end

    super
  end

778
779
780
781
  def project_setting
    super.presence || build_project_setting
  end

782
783
784
785
786
787
788
789
  def all_pipelines
    if builds_enabled?
      super
    else
      super.external
    end
  end