project.rb 42.3 KiB
Newer Older
gitlabhq's avatar
gitlabhq committed
class Project < ActiveRecord::Base
  include Gitlab::ConfigHelper
  include Gitlab::ShellAdapter
  include Gitlab::VisibilityLevel
  include Gitlab::CurrentSettings
  include AccessRequestable
  include Avatarable
  include Referable
  include Sortable
  include AfterCommitQueue
  include CaseSensitivity
  include ValidAttribute
  include ProjectFeaturesCompatibility
  include SelectForProjectAuthorization
  include Routable
  extend Gitlab::ConfigHelper
  BoardLimitExceeded = Class.new(StandardError)
Douwe Maan's avatar
Douwe Maan committed
  UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
  cache_markdown_field :description, pipeline: :description

  delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
           :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
  default_value_for :archived, false
  default_value_for :visibility_level, gitlab_config_features.visibility_level
  default_value_for :container_registry_enabled, gitlab_config_features.container_registry
  default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
  default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
  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
  default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
  after_create :ensure_storage_path_exist
  after_create :create_project_feature, unless: :project_feature
  after_save :update_project_statistics, if: :namespace_id_changed?
  # set last_activity_at to the same as created_at
  after_create :set_last_activity_at
    update_column(:last_activity_at, self.created_at)
  after_create :set_last_repository_updated_at
  def set_last_repository_updated_at
    update_column(:last_repository_updated_at, self.created_at)
  end

  before_destroy :remove_private_deploy_keys
Kamil Trzcinski's avatar
Kamil Trzcinski committed
  after_destroy :remove_pages

  # update visibility_level of forks
  after_update :update_forks_visibility_level

  after_validation :check_pending_delete

  after_save :ensure_storage_path_exist, if: :namespace_id_changed?
  attr_accessor :new_default_branch
  attr_accessor :old_path_with_namespace
  attr_accessor :template_name
  attr_writer :pipeline_status
  alias_attribute :title, :name

  belongs_to :creator, class_name: 'User'
  belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
  has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
  has_many :boards, before_add: :validate_board_limit
  # Project services
  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"
  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
  # Merge Requests for target project should be removed with it
  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
  has_many :project_authorizations
  has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
  has_many :project_members, -> { where(requested_at: nil) },
    as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
  alias_method :members, :project_members
  has_many :users, through: :project_members

  has_many :requesters, -> { where.not(requested_at: nil) },
    as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
  has_many :deploy_keys_projects
  has_many :deploy_keys, through: :deploy_keys_projects
  has_many :users_star_projects
Ciro Santilli's avatar
Ciro Santilli committed
  has_many :starrers, through: :users_star_projects, source: :user
  has_many :releases
  has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :lfs_objects, through: :lfs_objects_projects
  has_many :project_group_links
  has_many :invited_groups, through: :project_group_links, source: :group
  has_many :pages_domains
  has_many :todos
  has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
  has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
  has_one :project_feature
  has_one :statistics, class_name: 'ProjectStatistics'
  # 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.
  has_many :container_repositories, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  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.
  has_many :builds, class_name: 'Ci::Build', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  has_many :runner_projects, class_name: 'Ci::RunnerProject'
  has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
  has_many :variables, class_name: 'Ci::Variable'
  has_many :triggers, class_name: 'Ci::Trigger'
  has_many :environments
  has_many :deployments
  has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
  has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'

  accepts_nested_attributes_for :variables, allow_destroy: true
  accepts_nested_attributes_for :project_feature
  accepts_nested_attributes_for :import_data
  delegate :name, to: :owner, allow_nil: true, prefix: true
Douwe Maan's avatar
Douwe Maan committed
  delegate :count, to: :forks, prefix: true
  delegate :members, to: :team, prefix: true
  delegate :add_user, :add_users, to: :team
  delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
Douwe Maan's avatar
Douwe Maan committed
  delegate :empty_repo?, to: :repository
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
  # Validations
  validates :creator, presence: true, on: :create
  validates :description, length: { maximum: 2000 }, allow_blank: true
  validates :ci_config_path,
    format: { without: /\.{2}/,
              message: 'cannot include directory traversal.' },
    length: { maximum: 255 },
    allow_blank: true
  validates :name,
    presence: true,
    format: { with: Gitlab::Regex.project_name_regex,
Douwe Maan's avatar
Douwe Maan committed
              message: Gitlab::Regex.project_name_regex_message }
  validates :path,
    presence: true,
    format: { with: Gitlab::PathRegex.project_path_format_regex,
              message: Gitlab::PathRegex.project_path_format_message },
    uniqueness: { scope: :namespace_id }

  validates :namespace, presence: true
Douwe Maan's avatar
Douwe Maan committed
  validates :name, uniqueness: { scope: :namespace_id }
  validates :import_url, addressable_url: true, if: :external_import?
  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
  validates :star_count, numericality: { greater_than_or_equal_to: 0 }
  validate :check_limit, on: :create
    if: ->(project) { project.avatar.present? && project.avatar_changed? }
  validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
  validate :visibility_level_allowed_by_group
Douwe Maan's avatar
Douwe Maan committed
  validate :visibility_level_allowed_as_fork
  validate :check_wiki_path_conflict
  validates :repository_storage,
    presence: true,
    inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
  add_authentication_token_field :runners_token
  before_save :ensure_runners_token
Douwe Maan's avatar
Douwe Maan committed
  mount_uploader :avatar, AvatarUploader
  has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
  scope :pending_delete, -> { where(pending_delete: true) }
  scope :without_deleted, -> { where(pending_delete: false) }
  scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
  scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }

  scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
  scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
  scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
  scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
  scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
  scope :non_archived, -> { where(archived: false) }
  scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
  scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }

  scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
  scope :with_statistics, -> { includes(:statistics) }
  scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
  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.
    joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
      .where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")

  # "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) }
  scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
  enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
  # 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
      authorized = user
        .project_authorizations
        .select(1)
        .where('project_authorizations.project_id = projects.id')

      levels = Gitlab::VisibilityLevel.levels_for_user(user)

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

  # 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.
  #
  # 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.
  def self.with_feature_available_for_user(feature, user)
    visible = [nil, ProjectFeature::ENABLED]

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

      authorized = user.project_authorizations.select(1)
        .where('project_authorizations.project_id = projects.id')
      with_project_feature
        .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
              visible,
              ProjectFeature::PRIVATE,
              authorized)
    else
      with_feature_access_level(feature, visible)
    end
  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) }
  scope :excluding_project, ->(project) { where.not(id: project) }

  state_machine :import_status, initial: :none do
    event :import_schedule do
      transition [:none, :finished, :failed] => :scheduled
    end

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

    event :import_start do
    end

    event :import_finish do
      transition started: :finished
    end

    event :import_fail do
      transition [:scheduled, :started] => :failed
    end

    event :import_retry do
      transition failed: :started
    state :started
    state :finished
    after_transition [:none, :finished, :failed] => :scheduled do |project, _|
      project.run_after_commit { add_import_job }
    end

    after_transition started: :finished do |project, _|
      project.reset_cache_and_import_attrs

      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
            Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}")
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
  class << self
    # 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.
    def search(query)
      ptable  = arel_table
      ntable  = Namespace.arel_table
      pattern = "%#{query}%"

      # unscoping unnecessary conditions that'll be applied
      # when executing `where("projects.id IN (#{union.to_sql})")`
      projects = unscoped.select(:id).where(
        ptable[:path].matches(pattern)
          .or(ptable[:name].matches(pattern))
          .or(ptable[:description].matches(pattern))
      namespaces = unscoped.select(:id)
        .joins(:namespace)
        .where(ntable[:name].matches(pattern))

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

      where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
    end
    def search_by_title(query)
      pattern = "%#{query}%"
      table   = Project.arel_table

      non_archived.where(table[:name].matches(pattern))
    def visibility_levels
      Gitlab::VisibilityLevel.options
    end
      case method.to_s
      when 'storage_size_desc'
        # 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')
      when 'latest_activity_desc'
        reorder(last_activity_at: :desc)
      when 'latest_activity_asc'
        reorder(last_activity_at: :asc)
      else
        order_by(method)

    def reference_pattern
        ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
        (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
    def trending
      joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
        .reorder('trending_projects.id ASC')

    def cached_count
      Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
        Project.count
      end
    end
      joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
    return namespace.lfs_enabled? if self[:lfs_enabled].nil?
    self[:lfs_enabled] && Gitlab.config.lfs.enabled
    Gitlab.config.repositories.storages[repository_storage]['path']
  def team
    @team ||= ProjectTeam.new(self)
  end

  def repository
    @repository ||= Repository.new(full_path, self, disk_path: disk_path)
  def container_registry_url
Kamil Trzcinski's avatar
Kamil Trzcinski committed
    if Gitlab.config.registry.enabled
      "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
    return @images if defined?(@images)

    @images = container_repositories.to_a.any?(&:has_tags?) ||
      has_root_container_repository_tags?
  def commit(ref = 'HEAD')
    repository.commit(ref)
  # ref can't be HEAD, can only be branch/tag name or SHA
  def latest_successful_builds_for(ref = default_branch)
    latest_pipeline = pipelines.latest_successful_for(ref)

    if latest_pipeline
      latest_pipeline.builds.latest.with_artifacts
    else
      builds.none
    end
  def merge_base_commit(first_commit_id, second_commit_id)
Douwe Maan's avatar
Douwe Maan committed
    sha = repository.merge_base(first_commit_id, second_commit_id)
    repository.commit(sha) if sha
  def add_import_job
Douwe Maan's avatar
Douwe Maan committed
    job_id =
      if forked?
        RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
          forked_from_project.full_path,
Douwe Maan's avatar
Douwe Maan committed
          self.namespace.full_path)
      else
        RepositoryImportWorker.perform_async(self.id)
      end
      Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}"
      Rails.logger.error "Import job failed to start for #{full_path}"
  def reset_cache_and_import_attrs
    run_after_commit do
      ProjectCacheWorker.perform_async(self.id)
    end

    remove_import_data
  end

  # This method is overriden in EE::Project model
  def remove_import_data
    import_data&.destroy
  def ci_config_path=(value)
    # Strip all leading slashes so that //foo -> foo
Lin Jen-Shin's avatar
Lin Jen-Shin committed
    super(value&.sub(%r{\A/+}, '')&.delete("\0"))
  def import_url=(value)
    return super(value) unless Gitlab::UrlSanitizer.valid?(value)

    import_url = Gitlab::UrlSanitizer.new(value)
James Lopez's avatar
James Lopez committed
    super(import_url.sanitized_url)
    create_or_update_import_data(credentials: import_url.credentials)
  end

  def import_url
James Lopez's avatar
James Lopez committed
    if import_data && super.present?
      import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
James Lopez's avatar
James Lopez committed
      import_url.full_url
    else
      super
James Lopez's avatar
James Lopez committed
  def valid_import_url?
    valid? || errors.messages[:import_url].nil?
  end

  def create_or_update_import_data(data: nil, credentials: nil)
    return unless import_url.present? && valid_import_url?
James Lopez's avatar
James Lopez committed
    project_import_data = import_data || build_import_data
    if data
      project_import_data.data ||= {}
      project_import_data.data = project_import_data.data.merge(data)
    end
    if credentials
      project_import_data.credentials ||= {}
      project_import_data.credentials = project_import_data.credentials.merge(credentials)
    end
    external_import? || forked? || gitlab_project_import?
    import_url.present?
  end

    import_finished?
  end

  def import_in_progress?
    import_started? || import_scheduled?
  end

  def import_started?
    import? && import_status == 'started'
  end

  def import_scheduled?
    import_status == 'scheduled'
  end

  def import_failed?
    import_status == 'failed'
  end

  def import_finished?
    import_status == 'finished'
  def safe_import_url
    Gitlab::UrlSanitizer.new(import_url).masked_url
  def gitlab_project_import?
    import_type == 'gitlab_project'
  end

  def gitea_import?
    import_type == 'gitea'
  end

  def github_import?
    import_type == 'github'
  end

Douwe Maan's avatar
Douwe Maan committed
    unless creator.can_create_project? || namespace.kind == 'group'
      projects_limit = creator.projects_limit

      if projects_limit == 0
Phil Hughes's avatar
Phil Hughes committed
        self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions")
Phil Hughes's avatar
Phil Hughes committed
        self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it")
    self.errors.add(:base, "Can't check your ability to create project")
gitlabhq's avatar
gitlabhq committed
  end

  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.")
  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

    if persisted? && errors.include?(:path)
      path_was
    else
      path
    end
  # `from` argument can be a Namespace or Project.
  def to_reference(from = nil, full: false)
    if full || cross_namespace_reference?(from)
    elsif cross_project_reference?(from)
      path
    end
  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
    Gitlab::Routing.url_helpers.project_url(self)
  def new_issue_address(author)
    return unless Gitlab::IncomingEmail.supports_issue_creation? && author
    author.ensure_incoming_email_token!

    Gitlab::IncomingEmail.reply_address(
      "#{full_path}+#{author.incoming_email_token}")
  def build_commit_note(commit)
    notes.new(commit_id: commit.id, noteable_type: 'Commit')
gitlabhq's avatar
gitlabhq committed
  end
Nihad Abbasov's avatar
Nihad Abbasov committed

  def last_activity
gitlabhq's avatar
gitlabhq committed
  end

  def last_activity_date
    last_repository_updated_at || last_activity_at || updated_at
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  end
  def project_id
    self.id
  end
  def get_issue(issue_id, current_user)
    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
      ExternalIssue.new(issue_id, self)
Robert Speicher's avatar
Robert Speicher committed
  def issue_exists?(issue_id)
  def default_issue_tracker
    gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
  end

  def issues_tracker
    if external_issue_tracker
      external_issue_tracker
    else
      default_issue_tracker
    end
  end

  def external_issue_reference_pattern
    external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
  def default_issues_tracker?
    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?)
  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

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

    available_services_names = Service.available_services_names - exceptions

    available_services_names.map do |service_name|
      service = find_service(services, service_name)
        # We should check if template for the service exists
        template = find_service(services_templates, service_name)

        if template.nil?
          # If no template, we should create an instance. Ex `build_gitlab_ci_service`
          public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
          Service.build_from_template(id, template)
  def find_or_initialize_service(name)
    find_or_initialize_services.find { |service| service.to_param == name }
  end

  def create_labels
    Label.templates.each do |label|
      params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
      Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
  def find_service(list, name)
    list.find { |service| service.to_param == name }
  end
    services.where(category: :ci)
    @ci_service ||= ci_services.reorder(nil).find_by(active: true)
  def deployment_services
    services.where(category: :deployment)
  end

  def deployment_service
    @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
  def monitoring_services
    services.where(category: :monitoring)
  end

  def monitoring_service
    @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
Drew Blessing's avatar
Drew Blessing committed
  def jira_tracker?
    issues_tracker.to_param == 'jira'
  end

    unless self.avatar.image?
      self.errors.add :avatar, 'only images allowed'
    repository.avatar
  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
    avatar_path(args) || (Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git)
  def items_for(entity)
    case entity
    when 'issue' then
      issues
    when 'merge_request' then
      merge_requests
    end
  end
  def send_move_instructions(old_path_with_namespace)
    # 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) }
  def execute_hooks(data, hooks_scope = :push_hooks)
    hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
      hook.async_execute(data, hooks_scope.to_s)
  def execute_services(data, hooks_scope = :push_hooks)
    # Call only service hooks that are active for this scope
    services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
      service.async_execute(data)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
    end
  end

  def valid_repo?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  rescue
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
    false
  end

  def repo
Jacob Vosmaer's avatar
Jacob Vosmaer committed
    repository.rugged
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  end

  def url_to_repo
    gitlab_shell.url_to_repo(full_path)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  end

  def repo_exists?
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  rescue
    @repo_exists = false
  end

  def root_ref?(branch)
    repository.root_ref == branch
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
  end

  def ssh_url_to_repo
    url_to_repo
  end

  def http_url_to_repo
    "#{web_url}.git"
    !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
  def forked?
    !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?)
  end
  # Expires various caches before a project is renamed.
  def expire_caches_before_rename(old_path)
    repo = Repository.new(old_path, self)
    wiki = Repository.new("#{old_path}.wiki", self)

    if repo.exists?
  def hook_attrs(backward: true)
    attrs = {
Kirill Zaitsev's avatar
Kirill Zaitsev committed
      name: name,
Kirilll Zaitsev's avatar
Kirilll Zaitsev committed
      web_url: web_url,