project.rb 42.4 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
7
  include Gitlab::CurrentSettings
Rémy Coutable's avatar
Rémy Coutable committed
8
  include AccessRequestable
9
  include Avatarable
10
  include CacheMarkdownField
11
12
  include Referable
  include Sortable
13
  include AfterCommitQueue
14
  include CaseSensitivity
15
  include TokenAuthenticatable
16
  include ValidAttribute
Felipe Artur's avatar
Felipe Artur committed
17
  include ProjectFeaturesCompatibility
18
  include SelectForProjectAuthorization
19
  include Routable
20
  include Storage::LegacyProject
Robert Speicher's avatar
Robert Speicher committed
21

22
  extend Gitlab::ConfigHelper
23

24
  BoardLimitExceeded = Class.new(StandardError)
25

26
  NUMBER_OF_PERMITTED_BOARDS = 1
Douwe Maan's avatar
Douwe Maan committed
27
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
Jared Szechy's avatar
Jared Szechy committed
28

29
30
  cache_markdown_field :description, pipeline: :description

31
32
  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
33
                                                       allow_nil: true
Felipe Artur's avatar
Felipe Artur committed
34

35
  default_value_for :archived, false
36
  default_value_for :visibility_level, gitlab_config_features.visibility_level
37
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
38
  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
39
  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
40
41
42
43
44
  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
45
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
46

47
  after_create :ensure_storage_path_exist
48
  after_create :create_project_feature, unless: :project_feature
Markus Koller's avatar
Markus Koller committed
49
  after_save :update_project_statistics, if: :namespace_id_changed?
50

51
52
  # set last_activity_at to the same as created_at
  after_create :set_last_activity_at
53
  def set_last_activity_at
54
    update_column(:last_activity_at, self.created_at)
55
56
  end

57
58
59
60
61
  after_create :set_last_repository_updated_at
  def set_last_repository_updated_at
    update_column(:last_repository_updated_at, self.created_at)
  end

62
  before_destroy :remove_private_deploy_keys
Kamil Trzcinski's avatar
Kamil Trzcinski committed
63
64
  after_destroy :remove_pages

65
  # update visibility_level of forks
66
67
  after_update :update_forks_visibility_level

68
69
  after_validation :check_pending_delete

70
71
  # Legacy Storage specific hooks

72
  after_save :ensure_storage_path_exist, if: :namespace_id_changed?
73

74
  acts_as_taggable
75

76
  attr_accessor :new_default_branch
77
  attr_accessor :old_path_with_namespace
78
  attr_accessor :template_name
79
  attr_writer :pipeline_status
80

81
82
  alias_attribute :title, :name

83
  # Relations
84
  belongs_to :creator, class_name: 'User'
85
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
86
  belongs_to :namespace
87

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

91
  # Project services
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  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

  has_one  :forked_project_link,  foreign_key: "forked_to_project_id"
125
126
127
128
  has_one  :forked_from_project,  through:   :forked_project_link

  has_many :forked_project_links, foreign_key: "forked_from_project_id"
  has_many :forks,                through:     :forked_project_links, source: :forked_to_project
129

130
  # Merge Requests for target project should be removed with it
131
132
133
134
135
136
137
138
139
140
141
  has_many :merge_requests, foreign_key: 'target_project_id'
  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
142

143
  has_many :project_authorizations
144
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
145
  has_many :project_members, -> { where(requested_at: nil) },
146
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
147

148
  alias_method :members, :project_members
149
150
  has_many :users, through: :project_members

151
  has_many :requesters, -> { where.not(requested_at: nil) },
152
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
153

154
  has_many :deploy_keys_projects
155
  has_many :deploy_keys, through: :deploy_keys_projects
156
  has_many :users_star_projects
Ciro Santilli's avatar
Ciro Santilli committed
157
  has_many :starrers, through: :users_star_projects, source: :user
158
  has_many :releases
159
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Marin Jankovski's avatar
Marin Jankovski committed
160
  has_many :lfs_objects, through: :lfs_objects_projects
161
  has_many :project_group_links
162
  has_many :invited_groups, through: :project_group_links, source: :group
163
164
  has_many :pages_domains
  has_many :todos
165
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
166

167
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
168
169
  has_one :project_feature
  has_one :statistics, class_name: 'ProjectStatistics'
170

171
172
173
  # 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.
174
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
175

176
177
178
179
180
181
182
  has_many :commit_statuses
  has_many :pipelines, class_name: 'Ci::Pipeline'

  # 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.
183
  has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
184
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
185
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
186
  has_many :variables, class_name: 'Ci::Variable'
187
188
189
190
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
191

Kamil Trzcinski's avatar
Kamil Trzcinski committed
192
193
  has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'

194
  accepts_nested_attributes_for :variables, allow_destroy: true
Felipe Artur's avatar
Felipe Artur committed
195
  accepts_nested_attributes_for :project_feature
196
  accepts_nested_attributes_for :import_data
197

198
  delegate :name, to: :owner, allow_nil: true, prefix: true
199
  delegate :members, to: :team, prefix: true
200
  delegate :add_user, :add_users, to: :team
201
  delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
Douwe Maan's avatar
Douwe Maan committed
202
  delegate :empty_repo?, to: :repository
203

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
204
  # Validations
205
  validates :creator, presence: true, on: :create
206
  validates :description, length: { maximum: 2000 }, allow_blank: true
207
  validates :ci_config_path,
208
    format: { without: /\.{2}/,
209
              message: 'cannot include directory traversal.' },
210
211
    length: { maximum: 255 },
    allow_blank: true
212
213
  validates :name,
    presence: true,
214
    length: { maximum: 255 },
215
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
216
              message: Gitlab::Regex.project_name_regex_message }
217
218
  validates :path,
    presence: true,
219
    dynamic_path: true,
220
    length: { maximum: 255 },
221
222
    format: { with: Gitlab::PathRegex.project_path_format_regex,
              message: Gitlab::PathRegex.project_path_format_message },
223
224
    uniqueness: { scope: :namespace_id }

225
  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
226
  validates :name, uniqueness: { scope: :namespace_id }
227
  validates :import_url, addressable_url: true, if: :external_import?
228
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
229
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
230
  validate :check_limit, on: :create
231
  validate :avatar_type,
232
    if: ->(project) { project.avatar.present? && project.avatar_changed? }
233
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
Douwe Maan's avatar
Douwe Maan committed
234
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
235
  validate :visibility_level_allowed_as_fork
236
  validate :check_wiki_path_conflict
237
238
239
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
240

241
  add_authentication_token_field :runners_token
242
  before_save :ensure_runners_token
243

Douwe Maan's avatar
Douwe Maan committed
244
  mount_uploader :avatar, AvatarUploader
245
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
246

247
  # Scopes
248
  scope :pending_delete, -> { where(pending_delete: true) }
249
  scope :without_deleted, -> { where(pending_delete: false) }
250

251
  scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
252
253
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

254
  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
255
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
256
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
Toon Claes's avatar
Toon Claes committed
257
  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
258
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
259
  scope :non_archived, -> { where(archived: false) }
260
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
Rémy Coutable's avatar
Rémy Coutable committed
261
262
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }

263
  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
Markus Koller's avatar
Markus Koller committed
264
  scope :with_statistics, -> { includes(:statistics) }
265
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
266
267
268
  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.
269
270
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
271
  end
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286

  # "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) }
287
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
288

289
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
290

291
292
293
294
  # Returns a collection of projects that is either public or visible to the
  # logged in user.
  def self.public_or_visible_to_user(user = nil)
    if user
295
296
297
298
      authorized = user
        .project_authorizations
        .select(1)
        .where('project_authorizations.project_id = projects.id')
299
300
301
302
303
304
305
306
307

      levels = Gitlab::VisibilityLevel.levels_for_user(user)

      where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels)
    else
      public_to_user
    end
  end

308
309
310
  # 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.
311
312
313
314
  #
  # 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.
315
  def self.with_feature_available_for_user(feature, user)
316
317
318
319
320
321
322
    visible = [nil, ProjectFeature::ENABLED]

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

323
324
      authorized = user.project_authorizations.select(1)
        .where('project_authorizations.project_id = projects.id')
325

326
327
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
328
329
330
331
332
333
              visible,
              ProjectFeature::PRIVATE,
              authorized)
    else
      with_feature_access_level(feature, visible)
    end
334
  end
Felipe Artur's avatar
Felipe Artur committed
335

Rémy Coutable's avatar
Rémy Coutable committed
336
337
  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) }
338

339
340
  scope :excluding_project, ->(project) { where.not(id: project) }

341
  state_machine :import_status, initial: :none do
342
343
344
345
346
347
348
349
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

    event :force_import_start do
      transition [:none, :finished, :failed] => :started
    end

350
    event :import_start do
351
      transition scheduled: :started
352
353
354
    end

    event :import_finish do
355
      transition started: :finished
356
357
358
    end

    event :import_fail do
359
      transition [:scheduled, :started] => :failed
360
361
362
    end

    event :import_retry do
363
      transition failed: :started
364
365
    end

366
    state :scheduled
367
368
    state :started
    state :finished
369
370
    state :failed

371
372
373
374
    after_transition [:none, :finished, :failed] => :scheduled do |project, _|
      project.run_after_commit { add_import_job }
    end

375
376
    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs
377
378
379
380
381
382

      if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
        project.run_after_commit do
          begin
            Projects::HousekeepingService.new(project).execute
          rescue Projects::HousekeepingService::LeaseTaken => e
383
            Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}")
384
385
386
          end
        end
      end
387
    end
388
389
  end

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
390
  class << self
391
392
393
394
395
396
397
    # 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.
398
    def search(query)
399
      ptable  = arel_table
400
401
402
      ntable  = Namespace.arel_table
      pattern = "%#{query}%"

403
404
405
      # unscoping unnecessary conditions that'll be applied
      # when executing `where("projects.id IN (#{union.to_sql})")`
      projects = unscoped.select(:id).where(
406
407
408
        ptable[:path].matches(pattern)
          .or(ptable[:name].matches(pattern))
          .or(ptable[:description].matches(pattern))
409
410
      )

411
412
413
      namespaces = unscoped.select(:id)
        .joins(:namespace)
        .where(ntable[:name].matches(pattern))
414
415
416

      union = Gitlab::SQL::Union.new([projects, namespaces])

417
      where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
418
    end
419

420
    def search_by_title(query)
421
422
423
424
      pattern = "%#{query}%"
      table   = Project.arel_table

      non_archived.where(table[:name].matches(pattern))
425
426
    end

427
428
429
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
430
431

    def sort(method)
432
433
      case method.to_s
      when 'storage_size_desc'
Markus Koller's avatar
Markus Koller committed
434
435
436
        # 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')
437
438
439
440
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
441
442
      else
        order_by(method)
443
444
      end
    end
445
446

    def reference_pattern
447
      %r{
448
449
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
450
      }x
451
    end
452

453
    def trending
454
455
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')
456
    end
457
458
459
460
461
462

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

    def group_ids
465
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
466
    end
467
468
  end

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

472
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
473
474
  end

475
  def repository_storage_path
476
    Gitlab.config.repositories.storages[repository_storage]['path']
477
478
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
479
  def team
480
    @team ||= ProjectTeam.new(self)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
481
482
483
  end

  def repository
484
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
485
486
  end

Andre Guedes's avatar
Andre Guedes committed
487
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
488
    if Gitlab.config.registry.enabled
489
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
490
    end
491
492
  end

493
  def has_container_registry_tags?
494
495
496
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
497
      has_root_container_repository_tags?
498
499
  end

500
501
  def commit(ref = 'HEAD')
    repository.commit(ref)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
502
503
  end

504
  # ref can't be HEAD, can only be branch/tag name or SHA
505
  def latest_successful_builds_for(ref = default_branch)
506
    latest_pipeline = pipelines.latest_successful_for(ref)
507
508
509
510
511
512

    if latest_pipeline
      latest_pipeline.builds.latest.with_artifacts
    else
      builds.none
    end
513
514
  end

515
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
516
517
    sha = repository.merge_base(first_commit_id, second_commit_id)
    repository.commit(sha) if sha
518
519
  end

520
  def saved?
521
    id && persisted?
522
523
  end

524
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
525
526
527
    job_id =
      if forked?
        RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
528
          forked_from_project.full_path,
Douwe Maan's avatar
Douwe Maan committed
529
530
531
532
          self.namespace.full_path)
      else
        RepositoryImportWorker.perform_async(self.id)
      end
533
534

    if job_id
535
      Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}"
536
    else
537
      Rails.logger.error "Import job failed to start for #{full_path}"
538
    end
539
540
  end

541
542
543
544
545
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

546
547
548
549
550
    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
551
    import_data&.destroy
552
553
  end

554
  def ci_config_path=(value)
555
    # Strip all leading slashes so that //foo -> foo
Lin Jen-Shin's avatar
Lin Jen-Shin committed
556
    super(value&.sub(%r{\A/+}, '')&.delete("\0"))
557
558
  end

559
  def import_url=(value)
James Lopez's avatar
James Lopez committed
560
561
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

562
    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
563
    super(import_url.sanitized_url)
564
    create_or_update_import_data(credentials: import_url.credentials)
565
566
567
  end

  def import_url
James Lopez's avatar
James Lopez committed
568
    if import_data && super.present?
569
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
570
571
572
      import_url.full_url
    else
      super
573
574
    end
  end
575

James Lopez's avatar
James Lopez committed
576
577
578
579
  def valid_import_url?
    valid? || errors.messages[:import_url].nil?
  end

580
  def create_or_update_import_data(data: nil, credentials: nil)
581
    return unless import_url.present? && valid_import_url?
582

James Lopez's avatar
James Lopez committed
583
    project_import_data = import_data || build_import_data
James Lopez's avatar
James Lopez committed
584
585
586
587
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
588
589
590
591
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
592
  end
593

594
  def import?
595
    external_import? || forked? || gitlab_project_import?
596
597
  end

598
599
600
601
  def no_import?
    import_status == 'none'
  end

602
  def external_import?
603
604
605
    import_url.present?
  end

606
  def imported?
607
608
609
610
    import_finished?
  end

  def import_in_progress?
611
612
613
614
    import_started? || import_scheduled?
  end

  def import_started?
615
616
617
    import? && import_status == 'started'
  end

618
619
620
621
  def import_scheduled?
    import_status == 'scheduled'
  end

622
623
624
625
626
627
  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
628
629
  end

630
  def safe_import_url
631
    Gitlab::UrlSanitizer.new(import_url).masked_url
632
633
  end

634
635
636
637
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

Rémy Coutable's avatar
Rémy Coutable committed
638
639
640
641
  def gitea_import?
    import_type == 'gitea'
  end

642
643
644
645
  def github_import?
    import_type == 'github'
  end

646
  def check_limit
Douwe Maan's avatar
Douwe Maan committed
647
    unless creator.can_create_project? || namespace.kind == 'group'
648
649
650
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
651
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
652
      else
Phil Hughes's avatar
Phil Hughes committed
653
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
654
      end
655
656
    end
  rescue
Douwe Maan's avatar
Douwe Maan committed
657
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
658
659
  end

Douwe Maan's avatar
Douwe Maan committed
660
661
662
663
664
665
666
667
668
669
670
671
672
  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.")
673
674
  end

675
676
677
678
679
680
681
682
683
684
  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

685
  def to_param
686
687
688
689
690
    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
691
692
  end

693
  # `from` argument can be a Namespace or Project.
694
695
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
696
      full_path
697
698
699
    elsif cross_project_reference?(from)
      path
    end
700
701
702
703
704
705
706
707
  end

  def to_human_reference(from_project = nil)
    if cross_namespace_reference?(from_project)
      name_with_namespace
    elsif cross_project_reference?(from_project)
      name
    end
708
709
  end

710
  def web_url
711
    Gitlab::Routing.url_helpers.project_url(self)
712
713
  end

714
  def new_issue_address(author)
715
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
716

717
718
719
    author.ensure_incoming_email_token!

    Gitlab::IncomingEmail.reply_address(
720
      "#{full_path}+#{author.incoming_email_token}")
721
722
  end

723
  def build_commit_note(commit)
724
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
725
  end
Nihad Abbasov's avatar
Nihad Abbasov committed
726

727
  def last_activity
728
    last_event
gitlabhq's avatar
gitlabhq committed
729
730
731
  end

  def last_activity_date
732
    last_repository_updated_at || last_activity_at || updated_at
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
733
  end
734

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
735
736
737
  def project_id
    self.id
  end
randx's avatar
randx committed
738

739
  def get_issue(issue_id, current_user)
740
741
742
743
744
    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
745
      ExternalIssue.new(issue_id, self)
746
747
748
    end
  end

Robert Speicher's avatar
Robert Speicher committed
749
  def issue_exists?(issue_id)
750
    get_issue(issue_id)
Robert Speicher's avatar
Robert Speicher committed
751
752
  end

753
  def default_issue_tracker
754
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
755
756
757
758
759
760
761
762
763
764
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

765
  def external_issue_reference_pattern
766
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
767
768
  end

769
  def default_issues_tracker?
770
    !external_issue_tracker
771
772
773
  end

  def external_issue_tracker
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
    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
    update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
789
790
  end

791
792
793
794
  def has_wiki?
    wiki_enabled? || has_external_wiki?
  end

795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
  def external_wiki
    if has_external_wiki.nil?
      cache_has_external_wiki # Populate
    end

    if has_external_wiki
      @external_wiki ||= services.external_wikis.first
    else
      nil
    end
  end

  def cache_has_external_wiki
    update_column(:has_external_wiki, services.external_wikis.any?)
  end

811
  def find_or_initialize_services(exceptions: [])
812
813
    services_templates = Service.where(template: true)

814
815
816
    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
817
      service = find_service(services, service_name)
818

819
820
821
      if service
        service
      else
822
823
824
825
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
826
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
827
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
828
        else
829
          Service.build_from_template(id, template)
830
831
        end
      end
832
833
834
    end
  end

835
836
837
838
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

Valery Sizov's avatar
Valery Sizov committed
839
840
  def create_labels
    Label.templates.each do |label|
841
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
842
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
Valery Sizov's avatar
Valery Sizov committed
843
844
845
    end
  end

846
847
848
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
849

850
  def ci_services
851
    services.where(category: :ci)
852
853
854
  end

  def ci_service
855
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
856
857
  end

858
859
860
861
862
  def deployment_services
    services.where(category: :deployment)
  end

  def deployment_service
863
    @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
864
865
  end

866
867
868
869
870
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
871
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
872
873
  end

Drew Blessing's avatar
Drew Blessing committed
874
875
876
877
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

878
  def avatar_type
879
880
    unless self.avatar.image?
      self.errors.add :avatar, 'only images allowed'
881
882
883
884
    end
  end

  def avatar_in_git
885
    repository.avatar
886
887
  end

888
889
890
  def avatar_url(**args)
    # We use avatar_path instead of overriding avatar_url because of carrierwave.
    # See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
891
    avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
sue445's avatar
sue445 committed
892
893
  end

894
895
896
897
898
  # For compatibility with old code
  def code
    path
  end

899
  def items_for(entity)
900
901
902
903
904
905
906
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
907

908
  def send_move_instructions(old_path_with_namespace)
909
910
911
    # New project path needs to be committed to the DB or notification will
    # retrieve stale information
    run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
912
  end
913
914

  def owner
915
916
    if group
      group
917
    else
918
      namespace.try(:owner)
919
920
    end
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
921

922
  def execute_hooks(data, hooks_scope = :push_hooks)
923
    hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
924
      hook.async_execute(data, hooks_scope.to_s)
925
    end