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

gitlabhq's avatar
gitlabhq committed
3
class Project < ActiveRecord::Base
4
  include Gitlab::ConfigHelper
5
  include Gitlab::ShellAdapter
6
  include Gitlab::VisibilityLevel
Rémy Coutable's avatar
Rémy Coutable committed
7
  include AccessRequestable
8
  include Avatarable
9
  include CacheMarkdownField
10
11
  include Referable
  include Sortable
12
  include AfterCommitQueue
13
  include CaseSensitivity
14
  include TokenAuthenticatable
15
  include ValidAttribute
Felipe Artur's avatar
Felipe Artur committed
16
  include ProjectFeaturesCompatibility
17
  include SelectForProjectAuthorization
18
  include Presentable
19
  include Routable
20
  include GroupDescendant
21
  include Gitlab::SQL::Pattern
22
  include DeploymentPlatform
23
  include ::Gitlab::Utils::StrongMemoize
24
  include ChronicDurationAttribute
25
  include FastDestroyAll::Helpers
Robert Speicher's avatar
Robert Speicher committed
26

27
  extend Gitlab::ConfigHelper
28

29
  BoardLimitExceeded = Class.new(StandardError)
30

31
  NUMBER_OF_PERMITTED_BOARDS = 1
Douwe Maan's avatar
Douwe Maan committed
32
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
33
34
  # Hashed Storage versions handle rolling out new storage to project and dependents models:
  # nil: legacy
35
36
37
  # 1: repository
  # 2: attachments
  LATEST_STORAGE_VERSION = 2
38
39
40
41
  HASHED_STORAGE_FEATURES = {
    repository: 1,
    attachments: 2
  }.freeze
Jared Szechy's avatar
Jared Szechy committed
42

43
44
45
  # Valids ports to import from
  VALID_IMPORT_PORTS = [22, 80, 443].freeze

46
47
  cache_markdown_field :description, pipeline: :description

48
49
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
50
                                                       allow_nil: true
Felipe Artur's avatar
Felipe Artur committed
51

52
  delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
53

54
  default_value_for :archived, false
55
  default_value_for :visibility_level, gitlab_config_features.visibility_level
56
  default_value_for :resolve_outdated_diff_discussions, false
57
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
58
59
  default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
60
61
62
63
64
  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
65
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
66

67
68
  add_authentication_token_field :runners_token
  before_save :ensure_runners_token
69

70
  after_save :update_project_statistics, if: :namespace_id_changed?
71
72
73

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

74
  after_create :create_project_feature, unless: :project_feature
75
76
77
78
79

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

80
  after_create :set_last_activity_at
81
  after_create :set_last_repository_updated_at
82
  after_update :update_forks_visibility_level
83

84
  before_destroy :remove_private_deploy_keys
85

86
  use_fast_destroy :build_trace_chunks
87

88
  after_destroy -> { run_after_commit { remove_pages } }
89
  after_destroy :remove_exports
Kamil Trzcinski's avatar
Kamil Trzcinski committed
90

91
92
  after_validation :check_pending_delete

93
  # Storage specific hooks
94
  after_initialize :use_hashed_storage
95
  after_create :check_repository_absence!
96
97
  after_create :ensure_storage_path_exists
  after_save :ensure_storage_path_exists, if: :namespace_id_changed?
98

99
  acts_as_taggable
100

101
  attr_accessor :old_path_with_namespace
102
  attr_accessor :template_name
103
  attr_writer :pipeline_status
104
  attr_accessor :skip_disk_validation
105

106
107
  alias_attribute :title, :name

108
  # Relations
109
  belongs_to :creator, class_name: 'User'
110
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
111
  belongs_to :namespace
112
113
  alias_method :parent, :namespace
  alias_attribute :parent_id, :namespace_id
114

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

118
  # Project services
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
  has_one :campfire_service
  has_one :drone_ci_service
  has_one :emails_on_push_service
  has_one :pipelines_email_service
  has_one :irker_service
  has_one :pivotaltracker_service
  has_one :hipchat_service
  has_one :flowdock_service
  has_one :assembla_service
  has_one :asana_service
  has_one :gemnasium_service
  has_one :mattermost_slash_commands_service
  has_one :mattermost_service
  has_one :slack_slash_commands_service
  has_one :slack_service
  has_one :buildkite_service
  has_one :bamboo_service
  has_one :teamcity_service
  has_one :pushover_service
  has_one :jira_service
  has_one :redmine_service
  has_one :custom_issue_tracker_service
  has_one :bugzilla_service
  has_one :gitlab_issue_tracker_service, inverse_of: :project
  has_one :external_wiki_service
  has_one :kubernetes_service, inverse_of: :project
  has_one :prometheus_service, inverse_of: :project
  has_one :mock_ci_service
  has_one :mock_deployment_service
  has_one :mock_monitoring_service
  has_one :microsoft_teams_service
Matt Coleman's avatar
Matt Coleman committed
150
  has_one :packagist_service
151

152
  # TODO: replace these relations with the fork network versions
153
154
  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
  has_one  :forked_from_project,  through:   :forked_project_link
155
156

  has_many :forked_project_links, foreign_key: "forked_from_project_id"
157
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
158
159
160
161
162
163
164
  # TODO: replace these relations with the fork network versions

  has_one :root_of_fork_network,
          foreign_key: 'root_project_id',
          inverse_of: :root_project,
          class_name: 'ForkNetwork'
  has_one :fork_network_member
165
  has_one :fork_network, through: :fork_network_member
166

167
168
  has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project

169
  # Merge Requests for target project should be removed with it
170
  has_many :merge_requests, foreign_key: 'target_project_id'
171
  has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
172
173
174
175
176
177
178
179
180
181
  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
182

183
  has_many :project_authorizations
184
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
185
  has_many :project_members, -> { where(requested_at: nil) },
186
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
187

188
  alias_method :members, :project_members
189
  has_many :users, through: :project_members
190

191
  has_many :requesters, -> { where.not(requested_at: nil) },
192
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
193
  has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
194

195
  has_many :deploy_keys_projects
196
  has_many :deploy_keys, through: :deploy_keys_projects
197
  has_many :users_star_projects
198
  has_many :starrers, through: :users_star_projects, source: :user
199
  has_many :releases
200
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
201
  has_many :lfs_objects, through: :lfs_objects_projects
202
  has_many :lfs_file_locks
203
  has_many :project_group_links
204
  has_many :invited_groups, through: :project_group_links, source: :group
205
206
  has_many :pages_domains
  has_many :todos
207
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
208

209
210
  has_many :internal_ids

211
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
212
  has_one :project_feature, inverse_of: :project
213
  has_one :statistics, class_name: 'ProjectStatistics'
214

Shinya Maeda's avatar
Shinya Maeda committed
215
  has_one :cluster_project, class_name: 'Clusters::Project'
216
  has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
217

218
219
220
  # 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.
221
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
222

223
  has_many :commit_statuses
224
  has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
225
226
227
228
229

  # 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.
230
  has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
231
  has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
232
  has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
233
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
234
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
235
  has_many :variables, class_name: 'Ci::Variable'
236
237
238
239
  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
240
  has_many :project_deploy_tokens
241
  has_many :deploy_tokens, through: :project_deploy_tokens
242

243
  has_one :auto_devops, class_name: 'ProjectAutoDevops'
244
  has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
245

246
  has_many :project_badges, class_name: 'ProjectBadge'
247
  has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
248

249
  accepts_nested_attributes_for :variables, allow_destroy: true
250
  accepts_nested_attributes_for :project_feature, update_only: true
251
  accepts_nested_attributes_for :import_data
252
  accepts_nested_attributes_for :auto_devops, update_only: true
253

254
  delegate :name, to: :owner, allow_nil: true, prefix: true
255
  delegate :members, to: :team, prefix: true
256
  delegate :add_user, :add_users, to: :team
257
  delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team
258
  delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
259

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
260
  # Validations
261
  validates :creator, presence: true, on: :create
262
  validates :description, length: { maximum: 2000 }, allow_blank: true
263
  validates :ci_config_path,
264
    format: { without: %r{(\.{2}|\A/)},
265
              message: 'cannot include leading slash or directory traversal.' },
266
267
    length: { maximum: 255 },
    allow_blank: true
268
269
  validates :name,
    presence: true,
270
    length: { maximum: 255 },
271
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
272
              message: Gitlab::Regex.project_name_regex_message }
273
274
  validates :path,
    presence: true,
275
    project_path: true,
276
    length: { maximum: 255 }
277

278
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
279
  validates :name, uniqueness: { scope: :namespace_id }
280
  validates :import_url, addressable_url: true, if: :external_import?
281
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
282
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
283
  validate :check_limit, on: :create
284
  validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
Douwe Maan's avatar
Douwe Maan committed
285
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
286
  validate :visibility_level_allowed_as_fork
287
  validate :check_wiki_path_conflict
Rob Watson's avatar
Rob Watson committed
288
  validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
289
290
291
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
292
  validates :variables, variable_duplicates: { scope: :environment_scope }
293

294
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
295

296
  # Scopes
297
  scope :pending_delete, -> { where(pending_delete: true) }
298
  scope :without_deleted, -> { where(pending_delete: false) }
299

300
301
302
  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) }
303

304
305
  # 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") }
306
307
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

308
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
309
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
310
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
311
  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
312
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
313
  scope :archived, -> { where(archived: true) }
314
  scope :non_archived, -> { where(archived: false) }
315
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
Rémy Coutable's avatar
Rémy Coutable committed
316
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
317
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
Markus Koller's avatar
Markus Koller committed
318
  scope :with_statistics, -> { includes(:statistics) }
319
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
320
321
322
  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.
323
324
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
325
  end
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340

  # "enabled" here means "not disabled". It includes private features!
  scope :with_feature_enabled, ->(feature) {
    access_level_attribute = ProjectFeature.access_level_attribute(feature)
    with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
  }

  # Picks a feature where the level is exactly that given.
  scope :with_feature_access_level, ->(feature, level) {
    access_level_attribute = ProjectFeature.access_level_attribute(feature)
    with_project_feature.where(project_features: { access_level_attribute => level })
  }

  scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
  scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
341
  scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
342
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
343

344
345
346
347
348
  scope :with_group_runners_enabled, -> do
    joins(:ci_cd_settings)
    .where(project_ci_cd_settings: { group_runners_enabled: true })
  end

349
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
350

351
  chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600
352
353
354
355
356

  validates :build_timeout, allow_nil: true,
                            numericality: { greater_than_or_equal_to: 600,
                                            message: 'needs to be at least 10 minutes' }

357
358
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
359
360
  def self.public_or_visible_to_user(user = nil)
    if user
361
362
363
      where('EXISTS (?) OR projects.visibility_level IN (?)',
            user.authorizations_for_projects,
            Gitlab::VisibilityLevel.levels_for_user(user))
364
    else
365
      public_to_user
366
367
368
    end
  end

369
370
371
  # project features may be "disabled", "internal" or "enabled". If "internal",
  # they are only available to team members. This scope returns projects where
  # the feature is either enabled, or internal with permission for the user.
372
373
374
375
  #
  # 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.
376
  def self.with_feature_available_for_user(feature, user)
377
378
379
380
381
382
383
    visible = [nil, ProjectFeature::ENABLED]

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

384
385
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
386
387
              visible,
              ProjectFeature::PRIVATE,
388
              user.authorizations_for_projects)
389
390
391
    else
      with_feature_access_level(feature, visible)
    end
392
  end
Felipe Artur's avatar
Felipe Artur committed
393

Rémy Coutable's avatar
Rémy Coutable committed
394
395
  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) }
396

397
398
  scope :excluding_project, ->(project) { where.not(id: project) }

399
400
  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'") }
401

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
402
  class << self
403
404
405
406
407
408
409
    # 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.
410
    def search(query)
411
      fuzzy_search(query, [:path, :name, :description])
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
412
    end
413

414
    def search_by_title(query)
415
      non_archived.fuzzy_search(query, [:name])
416
417
    end

418
419
420
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
421

422
    def sort_by_attribute(method)
423
424
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
425
426
427
        # 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')
428
429
430
431
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
432
433
      else
        order_by(method)
434
435
      end
    end
436
437

    def reference_pattern
438
      %r{
439
440
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
441
      }x
442
    end
443

444
    def trending
445
446
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
447
    end
448
449
450
451
452
453

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

    def group_ids
456
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
457
    end
458
459
  end

460
461
462
463
464
465
466
  # 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

467
  def lfs_enabled?
468
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
Patricio Cano's avatar
Patricio Cano committed
469

470
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
471
472
  end

473
  def auto_devops_enabled?
474
    if auto_devops&.enabled.nil?
475
      Gitlab::CurrentSettings.auto_devops_enabled?
476
477
    else
      auto_devops.enabled?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
478
    end
479
480
  end

481
  def has_auto_devops_implicitly_disabled?
482
    auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
483
484
  end

485
486
487
488
  def empty_repo?
    repository.empty?
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
489
  def team
490
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
491
492
493
  end

  def repository
494
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
495
496
  end

497
498
  def cleanup
    @repository&.cleanup
499
500
501
    @repository = nil
  end

502
503
  alias_method :reload_repository!, :cleanup

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

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

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

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

521
522
523
524
  def commit_by(oid:)
    repository.commit_by(oid: oid)
  end

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

    if latest_pipeline
530
      latest_pipeline.builds.latest.with_artifacts_archive
531
532
533
    else
      builds.none
    end
534
535
  end

536
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
537
    sha = repository.merge_base(first_commit_id, second_commit_id)
538
    commit_by(oid: sha) if sha
539
540
  end

541
  def saved?
542
    id && persisted?
543
544
  end

545
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
546
547
    job_id =
      if forked?
548
        RepositoryForkWorker.perform_async(id)
549
      elsif gitlab_project_import?
James Lopez's avatar
James Lopez committed
550
        # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
551
        RepositoryImportWorker.set(retry: false).perform_async(self.id)
Douwe Maan's avatar
Douwe Maan committed
552
553
554
      else
        RepositoryImportWorker.perform_async(self.id)
      end
555

556
557
558
559
560
561
562
563
    log_import_activity(job_id)

    job_id
  end

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

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

571
572
573
574
575
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

576
    update(import_error: nil)
577
578
579
580
581
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
582
    import_data&.destroy
583
584
  end

585
  def ci_config_path=(value)
586
    # Strip all leading slashes so that //foo -> foo
587
    super(value&.delete("\0"))
588
589
  end

590
  def import_url=(value)
James Lopez's avatar
James Lopez committed
591
592
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

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

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

James Lopez's avatar
James Lopez committed
607
  def valid_import_url?
608
    valid?(:import_url) || errors.messages[:import_url].nil?
James Lopez's avatar
James Lopez committed
609
610
  end

611
  def create_or_update_import_data(data: nil, credentials: nil)
612
    return if data.nil? && credentials.nil?
613

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

620
621
622
623
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
624
  end
625

626
  def import?
James Lopez's avatar
James Lopez committed
627
    external_import? || forked? || gitlab_project_import? || bare_repository_import?
628
629
630
  end

  def external_import?
631
632
633
    import_url.present?
  end

634
  def imported?
635
636
637
638
    import_finished?
  end

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

642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
  def import_state_args
    {
      status: self[:import_status],
      jid: self[:import_jid],
      last_error: self[:import_error]
    }
  end

  def ensure_import_state
    return if self[:import_status] == 'none' || self[:import_status].nil?
    return unless import_state.nil?

    create_import_state(import_state_args)

    update_column(:import_status, 'none')
  end

  def import_schedule
    ensure_import_state

    import_state&.schedule
  end

  def force_import_start
    ensure_import_state

    import_state&.force_start
  end

  def import_start
    ensure_import_state

    import_state&.start
  end

  def import_fail
    ensure_import_state

    import_state&.fail_op
  end

  def import_finish
    ensure_import_state

    import_state&.finish
  end

  def import_jid=(new_jid)
    ensure_import_state

    import_state&.jid = new_jid
  end

  def import_jid
    ensure_import_state

    import_state&.jid
  end

  def import_error=(new_error)
    ensure_import_state

    import_state&.last_error = new_error
  end

  def import_error
    ensure_import_state

    import_state&.last_error
  end

  def import_status=(new_status)
    ensure_import_state

    import_state&.status = new_status
  end

  def import_status
    ensure_import_state

    import_state&.status || 'none'
  end

  def no_import?
    import_status == 'none'
  end

729
  def import_started?
730
731
    # import? does SQL work so only run it if it looks like there's an import running
    import_status == 'started' && import?
732
733
  end

734
735
736
737
  def import_scheduled?
    import_status == 'scheduled'
  end

738
739
740
741
742
743
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
744
745
  end

746
  def safe_import_url
747
    Gitlab::UrlSanitizer.new(import_url).masked_url
748
749
  end

750
751
752
753
  def bare_repository_import?
    import_type == 'bare_repository'
  end

754
755
756
757
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
758
759
760
761
  def gitea_import?
    import_type == 'gitea'
  end

762
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
763
    unless creator.can_create_project? || namespace.kind == 'group'
764
765
766
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
767
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
768
      else
Phil Hughes's avatar
Phil Hughes committed
769
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
770
      end
771
772
    end
  rescue
Douwe Maan's avatar
Douwe Maan committed
773
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
774
775
  end

Douwe Maan's avatar
Douwe Maan committed
776
777
778
779
780
781
782
783
784
785
786
787
788
  def visibility_level_allowed_by_group
    return if visibility_level_allowed_by_group?

    level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
    group_level_name = Gitlab::VisibilityLevel.level_name(self.group.visibility_level).downcase
    self.errors.add(:visibility_level, "#{level_name} is not allowed in a #{group_level_name} group.")
  end

  def visibility_level_allowed_as_fork
    return if visibility_level_allowed_as_fork?

    level_name = Gitlab::VisibilityLevel.level_name(self.visibility_level).downcase
    self.errors.add(:visibility_level, "#{level_name} is not allowed since the fork source project has lower visibility.")
789
790
  end

791
792
793
794
795
796
797
798
799
800
  def check_wiki_path_conflict
    return if path.blank?

    path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki"

    if Project.where(namespace_id: namespace_id, path: path_to_check).exists?
      errors.add(:name, 'has already been taken')
    end
  end

Rob Watson's avatar
Rob Watson committed
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
  def pages_https_only
    return false unless Gitlab.config.pages.external_https

    super
  end

  def pages_https_only?
    return false unless Gitlab.config.pages.external_https

    super
  end

  def validate_pages_https_only
    return unless pages_https_only?

    unless pages_domains.all?(&:https?)
      errors.add(:pages_https_only, "cannot be enabled unless all domains have TLS certificates")
    end
  end

821
  def to_param
822
823
824
825
826
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
827
828
  end

829
  # `from` argument can be a Namespace or Project.
830
831
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
832
      full_path
833
834
835
    elsif cross_project_reference?(from)
      path
    end
836
837
  end

838
839
  def to_human_reference(from = nil)
    if cross_namespace_reference?(from)
840
      name_with_namespace
841
    elsif cross_project_reference?(from)
842
843
      name
    end
844
845
  end

846
  def web_url
847
    Gitlab::Routing.url_helpers.project_url(self)
848
849
  end

850
  def new_issuable_address(author, address_type)
851
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
852

853
854
    author.ensure_incoming_email_token!

855
    suffix = address_type == 'merge_request' ? '+merge-request' : ''
856
    Gitlab::IncomingEmail.reply_address(
857
      "#{full_path}#{suffix}+#{author.incoming_email_token}")
858
859
  end

860
  def build_commit_note(commit)
861
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
862
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
863

864
  def last_activity
865
    last_event
gitlabhq's avatar
gitlabhq committed
866
867
868
  end

  def last_activity_date
869
    [last_activity_at, last_repository_updated_at, updated_at].compact.max
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
870
  end
871

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
872
873
874
  def project_id
    self.id
  end
randx's avatar
randx committed
875

876
  def get_issue(issue_id, current_user)
877
878
879
880
881
    issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?

    if issue
      issue
    elsif external_issue_tracker
Robert Speicher's avatar
Robert Speicher committed
882
      ExternalIssue.new(issue_id, self)
883
884
885
    end
  end

Robert Speicher's avatar
Robert Speicher committed
886
  def issue_exists?(issue_id)
887
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
888
889
  end

890
  def default_issue_tracker
891
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
892
893
894
895
896
897
898
899
900
901
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

902
  def external_issue_reference_pattern
903
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
904
905
  end

906
  def default_issues_tracker?
907
    !external_issue_tracker
908
909
910
  end

  def external_issue_tracker
911
912
913
914
915
916
917
918
919
920
921
922
923
924
    if has_external_issue_tracker.nil? # To populate existing projects
      cache_has_external_issue_tracker
    end

    if has_external_issue_tracker?
      return @external_issue_tracker if defined?(@external_issue_tracker)

      @external_issue_tracker = services.external_issue_trackers.first
    else
      nil
    end
  end

  def cache_has_external_issue_tracker