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

3
4
require 'carrierwave/orm/activerecord'

5
class Project < ApplicationRecord
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
18
  include ProjectAPICompatibility
Felipe Artur's avatar
Felipe Artur committed
19
  include ProjectFeaturesCompatibility
20
  include SelectForProjectAuthorization
21
  include Presentable
22
  include HasRepository
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
  extend Gitlab::Cache::RequestCache
Robert Speicher's avatar
Robert Speicher committed
37

38
  extend Gitlab::ConfigHelper
39

40
  BoardLimitExceeded = Class.new(StandardError)
41

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

54
55
56
57
58
  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
59

60
61
  ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10

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

65
66
  cache_markdown_field :description, pipeline: :description

67
  default_value_for :archived, false
68
  default_value_for :resolve_outdated_diff_discussions, false
69
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
70
71
  default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
72
73
74
75
76
  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
77
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
78
  default_value_for :remove_source_branch_after_merge, true
79
  default_value_for :autoclose_referenced_issues, true
80
  default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
81

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

84
  before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
85

86
  before_save :ensure_runners_token
87

Jasper Maes's avatar
Jasper Maes committed
88
  after_save :update_project_statistics, if: :saved_change_to_namespace_id?
89
90
91

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

92
  after_create :create_project_feature, unless: :project_feature
93
94
95
96
97

  after_create :create_ci_cd_settings,
    unless: :ci_cd_settings,
    if: proc { ProjectCiCdSetting.available? }

98
99
100
  after_create :create_container_expiration_policy,
               unless: :container_expiration_policy

101
  after_create :create_pages_metadatum,
102
               unless: :pages_metadatum
103

Mark Chao's avatar
Mark Chao committed
104
  after_create :set_timestamps_for_create
105
  after_update :update_forks_visibility_level
106

107
  before_destroy :remove_private_deploy_keys
108

109
  use_fast_destroy :build_trace_chunks
110

111
  after_destroy -> { run_after_commit { remove_pages } }
112
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
113

114
115
  after_validation :check_pending_delete

116
  # Storage specific hooks
117
  after_initialize :use_hashed_storage
118
  after_create :check_repository_absence!
119

120
  acts_as_ordered_taggable
121

122
  attr_accessor :old_path_with_namespace
123
  attr_accessor :template_name
124
  attr_writer :pipeline_status
125
  attr_accessor :skip_disk_validation
126

127
128
  alias_attribute :title, :name

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

137
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
138
  has_many :boards
Felipe Artur's avatar
Felipe Artur committed
139

140
  # Project services
141
  has_one :campfire_service
blackst0ne's avatar
blackst0ne committed
142
  has_one :discord_service
143
144
145
146
147
  has_one :drone_ci_service
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
148
  has_one :hipchat_service
149
150
151
152
153
154
155
156
157
158
159
160
161
  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
162
  has_one :youtrack_service
163
164
165
166
167
168
169
170
171
  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 :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
172
  has_one :packagist_service
173
  has_one :hangouts_chat_service
174
  has_one :unify_circuit_service
175

176
177
178
179
180
  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
181
  has_one :fork_network, through: :fork_network_member
182
183
184
  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
185
  has_many :fork_network_projects, through: :fork_network, source: :projects
186

187
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
188
  has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
189
  has_one :project_repository, inverse_of: :project
190
  has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
191
  has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
192
  has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
193
  has_one :grafana_integration, inverse_of: :project
194
  has_one :project_setting, ->(project) { where_or_create_by(project: project) }, inverse_of: :project
195

196
  # Merge Requests for target project should be removed with it
197
  has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
198
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
199
200
201
202
203
204
205
206
207
208
  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
209
  has_many :repository_languages, -> { order "share DESC" }
210

211
  has_many :project_authorizations
212
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
213
  has_many :project_members, -> { where(requested_at: nil) },
214
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
215

216
  alias_method :members, :project_members
217
  has_many :users, through: :project_members
218

219
  has_many :requesters, -> { where.not(requested_at: nil) },
220
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
221
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
222

223
  has_many :deploy_keys_projects, inverse_of: :project
224
  has_many :deploy_keys, through: :deploy_keys_projects
225
  has_many :users_star_projects
226
  has_many :starrers, through: :users_star_projects, source: :user
227
  has_many :releases
228
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
229
  has_many :lfs_objects, -> { distinct }, through: :lfs_objects_projects
230
  has_many :lfs_file_locks
231
  has_many :project_group_links
232
  has_many :invited_groups, through: :project_group_links, source: :group
233
234
  has_many :pages_domains
  has_many :todos
235
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
236

237
238
  has_many :internal_ids

239
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
240
  has_one :project_feature, inverse_of: :project
241
  has_one :statistics, class_name: 'ProjectStatistics'
242

Shinya Maeda's avatar
Shinya Maeda committed
243
  has_one :cluster_project, class_name: 'Clusters::Project'
244
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
245
  has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
246
  has_many :management_clusters, class_name: 'Clusters::Cluster', foreign_key: :management_project_id, inverse_of: :management_project
247

248
  has_many :prometheus_metrics
249
  has_many :prometheus_alerts, inverse_of: :project
250

251
252
253
  # 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.
254
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
255
  has_one :container_expiration_policy, inverse_of: :project
256

257
  has_many :commit_statuses
258
  # The relation :all_pipelines is intended to be used when we want to get the
259
260
  # whole list of pipelines associated to the project
  has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
261
  # The relation :ci_pipelines is intended to be used when we want to get only
262
263
264
265
  # 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,
266
          -> { ci_sources },
267
268
          class_name: 'Ci::Pipeline',
          inverse_of: :project
269
  has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
270
271
272
273
274

  # 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.
275
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
276
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
277
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
278
  has_many :job_artifacts, class_name: 'Ci::JobArtifact'
279
  has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
280
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
281
  has_many :variables, class_name: 'Ci::Variable'
282
283
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
284
  has_many :environments_for_dashboard, -> { from(with_rank.unfoldered.available, :environments).where('rank <= 3') }, class_name: 'Environment'
285
  has_many :deployments
286
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
Mayra Cabrera's avatar
Mayra Cabrera committed
287
  has_many :project_deploy_tokens
288
  has_many :deploy_tokens, through: :project_deploy_tokens
289
  has_many :resource_groups, class_name: 'Ci::ResourceGroup', inverse_of: :project
290

Nick Thomas's avatar
Nick Thomas committed
291
  has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true
292
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
293

294
  has_many :project_badges, class_name: 'ProjectBadge'
295
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
296

297
  has_many :remote_mirrors, inverse_of: :project
298
  has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage'
299

300
301
  has_many :external_pull_requests, inverse_of: :project

302
303
304
  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

305
306
  has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project

307
308
  has_many :import_failures, inverse_of: :project

309
  accepts_nested_attributes_for :variables, allow_destroy: true
310
  accepts_nested_attributes_for :project_feature, update_only: true
311
  accepts_nested_attributes_for :import_data
312
  accepts_nested_attributes_for :auto_devops, update_only: true
313
  accepts_nested_attributes_for :ci_cd_settings, update_only: true
314
  accepts_nested_attributes_for :container_expiration_policy, update_only: true
315

316
317
318
319
  accepts_nested_attributes_for :remote_mirrors,
                                allow_destroy: true,
                                reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }

320
  accepts_nested_attributes_for :incident_management_setting, update_only: true
321
  accepts_nested_attributes_for :error_tracking_setting, update_only: true
322
  accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
323
  accepts_nested_attributes_for :grafana_integration, update_only: true, allow_destroy: true
324

325
326
327
328
329
  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,
330
    :repository_access_level, :pages_access_level,
331
332
333
334
    to: :project_feature, allow_nil: true
  delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
    prefix: :import, to: :import_state, allow_nil: true
  delegate :no_import?, to: :import_state, allow_nil: true
335
  delegate :name, to: :owner, allow_nil: true, prefix: true
336
  delegate :members, to: :team, prefix: true
337
  delegate :add_user, :add_users, to: :team
338
339
  delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
  delegate :add_master, to: :team # @deprecated
340
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
341
  delegate :root_ancestor, to: :namespace, allow_nil: true
342
  delegate :last_pipeline, to: :commit, allow_nil: true
343
  delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
344
  delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
345

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
346
  # Validations
347
  validates :creator, presence: true, on: :create
348
  validates :description, length: { maximum: 2000 }, allow_blank: true
349
  validates :ci_config_path,
350
    format: { without: %r{(\.{2}|\A/)},
351
              message: _('cannot include leading slash or directory traversal.') },
352
353
    length: { maximum: 255 },
    allow_blank: true
354
355
  validates :name,
    presence: true,
356
    length: { maximum: 255 },
357
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
358
              message: Gitlab::Regex.project_name_regex_message }
359
360
  validates :path,
    presence: true,
361
    project_path: true,
362
    length: { maximum: 255 }
363

364
365
  validates :project_feature, presence: true

366
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
367
  validates :name, uniqueness: { scope: :namespace_id }
368
  validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
369
370
                                       ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
                                       enforce_user: true }, if: [:external_import?, :import_url_changed?]
371
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
372
  validate :check_personal_projects_limit, on: :create
373
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
374
375
  validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
  validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
376
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
377
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
378
379
380
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
381
  validates :variables, variable_duplicates: { scope: :environment_scope }
382
  validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
383
  validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
384

385
  # Scopes
386
  scope :pending_delete, -> { where(pending_delete: true) }
387
  scope :without_deleted, -> { where(pending_delete: false) }
388

389
390
391
392
393
394
395
396
397
398
399
  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
400

401
  # 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
402
  scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) }
403
404
  scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
  scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
405
  scope :sorted_by_name_asc_limited, ->(limit) { reorder(name: :asc).limit(limit) }
406
  # Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
407
  scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
408

409
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
410
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
411
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
412
  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
413
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
414
  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)) }
415
  scope :archived, -> { where(archived: true) }
416
  scope :non_archived, -> { where(archived: false) }
417
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
Rémy Coutable's avatar
Rémy Coutable committed
418
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
419
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
420
  scope :inc_routes, -> { includes(:route, namespace: :route) }
Markus Koller's avatar
Markus Koller committed
421
  scope :with_statistics, -> { includes(:statistics) }
422
423
  scope :with_namespace, -> { includes(:namespace) }
  scope :with_import_state, -> { includes(:import_state) }
424
  scope :with_service, ->(service) { joins(service).eager_load(service) }
425
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
426
  scope :with_container_registry, -> { where(container_registry_enabled: true) }
427
428
429
  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.
430
431
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
432
  end
433
434
435

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
436
437
438
439
    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)
440
441
442
443
444
445
446
447
  }

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

448
449
450
451
452
453
454
455
456
457
  # 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

458
459
  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
460
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
461
  scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
462
463
464
  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
465
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
466
  scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
467
  scope :with_limit, -> (maximum) { limit(maximum) }
468

469
470
471
472
473
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

474
475
476
477
  scope :with_pages_deployed, -> do
    joins(:pages_metadatum).merge(ProjectPagesMetadatum.deployed)
  end

478
479
480
481
482
  scope :pages_metadata_not_migrated, -> do
    left_outer_joins(:pages_metadatum)
      .where(project_pages_metadata: { project_id: nil })
  end

483
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
484

485
  chronic_duration_attr :build_timeout_human_readable, :build_timeout,
486
    default: 3600, error_message: _('Maximum job timeout has a value which could not be accepted')
487
488

  validates :build_timeout, allow_nil: true,
489
                            numericality: { greater_than_or_equal_to: 10.minutes,
490
                                            less_than: MAX_BUILD_TIMEOUT,
491
                                            only_integer: true,
492
                                            message: _('needs to be between 10 minutes and 1 month') }
493

494
495
496
  # Used by Projects::CleanupService to hold a map of rewritten object IDs
  mount_uploader :bfg_object_map, AttachmentUploader

497
498
499
500
  def self.eager_load_namespace_and_owner
    includes(namespace: :owner)
  end

501
502
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
503
504
505
  def self.public_or_visible_to_user(user = nil, min_access_level = nil)
    min_access_level = nil if user&.admin?

506
507
    if user
      where('EXISTS (?) OR projects.visibility_level IN (?)',
508
            user.authorizations_for_projects(min_access_level: min_access_level),
509
510
511
            Gitlab::VisibilityLevel.levels_for_user(user))
    else
      public_to_user
512
513
514
    end
  end

515
  # project features may be "disabled", "internal", "enabled" or "public". If "internal",
516
  # they are only available to team members. This scope returns projects where
517
  # the feature is either public, enabled, or internal with permission for the user.
518
519
  # 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
520
  # that checks project authorizations first (e.g. `filter_by_feature_visibility`).
521
522
523
524
  #
  # 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.
525
  def self.with_feature_available_for_user(feature, user)
526
    visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
527
528
529
530

    if user&.admin?
      with_feature_enabled(feature)
    elsif user
531
      min_access_level = ProjectFeature.required_minimum_access_level(feature)
532
533
      column = ProjectFeature.quoted_access_level_column(feature)

534
      with_project_feature
535
536
537
538
539
540
      .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)
            })
541
    else
542
543
      # This has to be added to include features whose value is nil in the db
      visible << nil
544
545
      with_feature_access_level(feature, visible)
    end
546
  end
Felipe Artur's avatar
Felipe Artur committed
547

548
549
  # This scope returns projects where user has access to both the project and the feature.
  def self.filter_by_feature_visibility(feature, user)
550
551
552
553
554
    with_feature_available_for_user(feature, user)
      .public_or_visible_to_user(
        user,
        ProjectFeature.required_minimum_access_level_for_private_project(feature)
      )
555
556
  end

557
558
  def self.wrap_with_cte(collection)
    cte = Gitlab::SQL::CTE.new(:projects_cte, collection)
559
560
561
    Project.with(cte.to_arel).from(cte.alias_to(Project.arel_table))
  end

Rémy Coutable's avatar
Rémy Coutable committed
562
563
  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) }
564

565
566
  scope :excluding_project, ->(project) { where.not(id: project) }

567
568
  # 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") }
569
  scope :for_group, -> (group) { where(group: group) }
570
  scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) }
571

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
572
  class << self
573
574
575
576
577
578
579
    # 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.
580
    def search(query)
581
582
583
584
585
      if Feature.enabled?(:project_search_by_full_path, default_enabled: true)
        joins(:route).fuzzy_search(query, [Route.arel_table[:path], :name, :description])
      else
        fuzzy_search(query, [:path, :name, :description])
      end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
586
    end
587

588
    def search_by_title(query)
589
      non_archived.fuzzy_search(query, [:name])
590
591
    end

592
593
594
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
595

596
    def sort_by_attribute(method)
597
598
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
599
600
601
        # 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')
602
      when 'latest_activity_desc'
603
        reorder(self.arel_table['last_activity_at'].desc)
604
      when 'latest_activity_asc'
605
        reorder(self.arel_table['last_activity_at'].asc)
606
      when 'stars_desc'
607
        sorted_by_stars_desc
608
609
      when 'stars_asc'
        sorted_by_stars_asc
610
611
      else
        order_by(method)
612
613
      end
    end
614
615

    def reference_pattern
616
      %r{
617
        (?<!#{Gitlab::PathRegex::PATH_START_CHAR})
618
619
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
620
      }x
621
    end
622

623
624
625
626
    def reference_postfix
      '>'
    end

627
628
629
630
    def reference_postfix_escaped
      '&gt;'
    end

631
    # Pattern used to extract `namespace/project>` project references from text.
632
633
    # '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
    # when the reference comes from an external source.
634
635
636
    def markdown_reference_pattern
      %r{
        #{reference_pattern}
637
        (#{reference_postfix}|#{reference_postfix_escaped})
638
639
640
      }x
    end

641
    def trending
642
643
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
644
    end
645
646
647
648
649
650

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

    def group_ids
653
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
654
    end
655

656
    # Returns ids of projects with issuables available for given user
657
    #
658
659
660
    # 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)
661
662
663
664
665
      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
666
667
  end

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
668
  def initialize(attributes = nil)
669
670
671
672
673
674
675
676
677
    # 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
678
679
    attributes ||= {}

680
681
682
683
684
685
686
    unless visibility_attribute_present?(attributes)
      attributes[:visibility_level] = Gitlab::CurrentSettings.default_project_visibility
    end

    super
  end

687
688
689
690
691
692
693
694
  def all_pipelines
    if builds_enabled?
      super
    else
      super.external
    end
  end

695
696
697
698
699
700
701
702
  def ci_pipelines
    if builds_enabled?
      super
    else
      super.external
    end
  end

703
704
705
706
707
708
  def autoclose_referenced_issues
    return true if super.nil?

    super
  end

709
710
711
712
713
  def preload_protected_branches
    preloader = ActiveRecord::Associations::Preloader.new
    preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels])
  end

714
715
  # returns all ancestor-groups upto but excluding the given namespace
  # when no namespace is given, all ancestors upto the top are returned
716
  def ancestors_upto(top = nil, hierarchy_order: nil)
717
    Gitlab::ObjectHierarchy.new(Group.where(id: namespace_id))
718
      .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
719
720
  end

721
722
  alias_method :ancestors, :ancestors_upto

723
724
725
  def emails_disabled?
    strong_memoize(:emails_disabled) do
      # disabling in the namespace overrides the project setting
726
      super || namespace.emails_disabled?
727
728
729
    end
  end

730
  def lfs_enabled?
731
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
732

733
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
734
735
  end

Cindy Pallares's avatar
Cindy Pallares committed
736
737
  alias_method :lfs_enabled, :lfs_enabled?

738
  def auto_devops_enabled?
739
    if auto_devops&.enabled.nil?
740
      has_auto_devops_implicitly_enabled?
741
742
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
743
    end
744
745
  end

746
  def has_auto_devops_implicitly_enabled?
747
748
749
    auto_devops_config = first_auto_devops_config

    auto_devops_config[:scope] != :project && auto_devops_config[:status]
750
751
  end

752
  def has_auto_devops_implicitly_disabled?
753
754
755
756
757
758
759
760
761
    auto_devops_config = first_auto_devops_config

    auto_devops_config[:scope] != :project && !auto_devops_config[:status]
  end

  def first_auto_devops_config
    return namespace.first_auto_devops_config if auto_devops&.enabled.nil?

    { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
762
763
  end

Jacopo's avatar
Jacopo committed
764
765
766
767
  def daily_statistics_enabled?
    Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
  end

768
  def unlink_forks_upon_visibility_decrease_enabled?
769
    Feature.enabled?(:unlink_fork_network_upon_visibility_decrease, self, default_enabled: true)
770
771
  end

772
773
774
775
  def context_commits_enabled?
    Feature.enabled?(:context_commits, default_enabled: true)
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
776
  def team
777
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
778
779
780
  end

  def repository
781
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
782
783
  end

784
  def cleanup
785
786
787
    @repository = nil
  end

788
789
  alias_method :reload_repository!, :cleanup

Andre Guedes's avatar
Andre Guedes committed
790
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
791
    if Gitlab.config.registry.enabled
792
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
793
    end
794
795
  end

796
  def has_container_registry_tags?
797
798
799
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
800
      has_root_container_repository_tags?
801
802
  end

Matija Čupić's avatar
Matija Čupić committed
803
  # ref can't be HEAD, can only be branch/tag name
804
  def latest_successful_build_for_ref(job_name, ref = default_branch)
Matija Čupić's avatar
Matija Čupić committed
805
806
    return unless ref

807
    latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
808
    return unless latest_pipeline
809