debian-mirror-gitlab/app/models/project.rb

2281 lines
69 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2015-04-26 12:48:37 +05:30
require 'carrierwave/orm/activerecord'
2019-07-07 11:18:12 +05:30
class Project < ApplicationRecord
2015-09-11 14:41:01 +05:30
include Gitlab::ConfigHelper
2014-09-02 18:07:02 +05:30
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include AccessRequestable
2017-09-10 17:25:29 +05:30
include Avatarable
2016-11-03 12:29:30 +05:30
include CacheMarkdownField
2015-09-11 14:41:01 +05:30
include Referable
include Sortable
2015-10-24 18:46:33 +05:30
include AfterCommitQueue
include CaseSensitivity
include TokenAuthenticatable
2017-08-17 22:00:37 +05:30
include ValidAttribute
2016-09-29 09:46:39 +05:30
include ProjectFeaturesCompatibility
2017-08-17 22:00:37 +05:30
include SelectForProjectAuthorization
2018-03-27 19:54:05 +05:30
include Presentable
2017-08-17 22:00:37 +05:30
include Routable
2018-03-17 18:26:18 +05:30
include GroupDescendant
include Gitlab::SQL::Pattern
include DeploymentPlatform
include ::Gitlab::Utils::StrongMemoize
2018-05-09 12:01:36 +05:30
include ChronicDurationAttribute
2018-10-15 14:42:47 +05:30
include FastDestroyAll::Helpers
2018-11-08 19:23:39 +05:30
include WithUploads
include BatchDestroyDependentAssociations
2018-11-20 20:47:30 +05:30
include FeatureGate
include OptionallySearch
2018-12-05 23:21:45 +05:30
include FromUnion
2019-02-15 15:39:39 +05:30
include IgnorableColumn
2018-11-08 19:23:39 +05:30
extend Gitlab::Cache::RequestCache
2015-12-23 02:04:40 +05:30
2014-09-02 18:07:02 +05:30
extend Gitlab::ConfigHelper
2017-08-17 22:00:37 +05:30
BoardLimitExceeded = Class.new(StandardError)
2016-11-03 12:29:30 +05:30
2018-11-18 11:00:15 +05:30
STATISTICS_ATTRIBUTE = 'repositories_count'.freeze
2017-08-17 22:00:37 +05:30
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
2018-03-17 18:26:18 +05:30
# Hashed Storage versions handle rolling out new storage to project and dependents models:
# nil: legacy
# 1: repository
# 2: attachments
LATEST_STORAGE_VERSION = 2
HASHED_STORAGE_FEATURES = {
repository: 1,
attachments: 2
}.freeze
2018-03-26 14:24:53 +05:30
2018-12-13 13:39:08 +05:30
VALID_IMPORT_PORTS = [80, 443].freeze
VALID_IMPORT_PROTOCOLS = %w(http https git).freeze
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
2015-09-25 12:07:36 +05:30
2019-02-15 15:39:39 +05:30
ignore_column :import_status, :import_jid, :import_error
2016-11-03 12:29:30 +05:30
cache_markdown_field :description, pipeline: :description
2017-08-17 22:00:37 +05:30
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
2018-12-05 23:21:45 +05:30
:merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?,
to: :project_feature, allow_nil: true
2016-09-29 09:46:39 +05:30
2018-03-17 18:26:18 +05:30
delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
2019-02-15 15:39:39 +05:30
delegate :scheduled?, :started?, :in_progress?,
:failed?, :finished?,
prefix: :import, to: :import_state, allow_nil: true
delegate :no_import?, to: :import_state, allow_nil: true
2014-09-02 18:07:02 +05:30
default_value_for :archived, false
2019-03-02 22:35:43 +05:30
default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility }
2018-03-17 18:26:18 +05:30
default_value_for :resolve_outdated_diff_discussions, false
2016-06-02 11:05:42 +05:30
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
2018-03-17 18:26:18 +05:30
default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
2016-11-24 13:41:30 +05:30
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
2017-08-17 22:00:37 +05:30
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
2014-09-02 18:07:02 +05:30
2019-07-07 11:18:12 +05:30
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
2018-10-15 14:42:47 +05:30
2018-11-08 19:23:39 +05:30
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
2018-10-15 14:42:47 +05:30
2018-03-17 18:26:18 +05:30
before_save :ensure_runners_token
2016-08-24 12:49:21 +05:30
2018-03-17 18:26:18 +05:30
after_save :update_project_statistics, if: :namespace_id_changed?
2018-10-15 14:42:47 +05:30
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
2018-03-17 18:26:18 +05:30
after_create :create_project_feature, unless: :project_feature
2018-10-15 14:42:47 +05:30
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
2018-12-13 13:39:08 +05:30
after_create :set_timestamps_for_create
2018-03-17 18:26:18 +05:30
after_update :update_forks_visibility_level
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
before_destroy :remove_private_deploy_keys
2018-10-15 14:42:47 +05:30
use_fast_destroy :build_trace_chunks
2017-09-10 17:25:29 +05:30
after_destroy -> { run_after_commit { remove_pages } }
2018-03-17 18:26:18 +05:30
after_destroy :remove_exports
2017-08-17 22:00:37 +05:30
after_validation :check_pending_delete
2018-03-17 18:26:18 +05:30
# Storage specific hooks
after_initialize :use_hashed_storage
after_create :check_repository_absence!
after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed?
2017-09-10 17:25:29 +05:30
2018-12-05 23:21:45 +05:30
acts_as_ordered_taggable
2014-09-02 18:07:02 +05:30
attr_accessor :old_path_with_namespace
2017-09-10 17:25:29 +05:30
attr_accessor :template_name
2017-08-17 22:00:37 +05:30
attr_writer :pipeline_status
2018-03-17 18:26:18 +05:30
attr_accessor :skip_disk_validation
2014-09-02 18:07:02 +05:30
2016-06-02 11:05:42 +05:30
alias_attribute :title, :name
2014-09-02 18:07:02 +05:30
# Relations
2018-12-13 13:39:08 +05:30
belongs_to :pool_repository
2017-08-17 22:00:37 +05:30
belongs_to :creator, class_name: 'User'
2016-09-29 09:46:39 +05:30
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
2014-09-02 18:07:02 +05:30
belongs_to :namespace
2018-03-17 18:26:18 +05:30
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
2014-09-02 18:07:02 +05:30
2017-08-17 22:00:37 +05:30
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
2019-07-07 11:18:12 +05:30
has_many :boards
2016-09-29 09:46:39 +05:30
2014-09-02 18:07:02 +05:30
# Project services
2017-09-10 17:25:29 +05:30
has_one :campfire_service
2019-02-15 15:39:39 +05:30
has_one :discord_service
2017-09-10 17:25:29 +05:30
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 :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
2019-07-07 11:18:12 +05:30
has_one :youtrack_service
2017-09-10 17:25:29 +05:30
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
2018-03-17 18:26:18 +05:30
has_one :packagist_service
2018-11-18 11:00:15 +05:30
has_one :hangouts_chat_service
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
inverse_of: :root_project,
class_name: 'ForkNetwork'
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
2018-12-13 13:39:08 +05:30
has_one :forked_from_project, through: :fork_network_member
has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
2015-04-26 12:48:37 +05:30
2018-10-15 14:42:47 +05:30
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
2018-11-08 19:23:39 +05:30
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
2019-02-15 15:39:39 +05:30
has_one :project_repository, inverse_of: :project
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
2018-10-15 14:42:47 +05:30
2014-09-02 18:07:02 +05:30
# Merge Requests for target project should be removed with it
2018-12-13 13:39:08 +05:30
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
2018-03-27 19:54:05 +05:30
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
2017-09-10 17:25:29 +05:30
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
2018-11-18 11:00:15 +05:30
has_many :repository_languages, -> { order "share DESC" }
2016-08-24 12:49:21 +05:30
2017-08-17 22:00:37 +05:30
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
2017-09-10 17:25:29 +05:30
has_many :project_members, -> { where(requested_at: nil) },
as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :project_members
2016-08-24 12:49:21 +05:30
has_many :users, through: :project_members
2017-09-10 17:25:29 +05:30
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2018-03-17 18:26:18 +05:30
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
has_many :deploy_keys_projects
2014-09-02 18:07:02 +05:30
has_many :deploy_keys, through: :deploy_keys_projects
2017-09-10 17:25:29 +05:30
has_many :users_star_projects
2014-09-02 18:07:02 +05:30
has_many :starrers, through: :users_star_projects, source: :user
2017-09-10 17:25:29 +05:30
has_many :releases
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
2015-11-26 14:37:03 +05:30
has_many :lfs_objects, through: :lfs_objects_projects
2018-03-17 18:26:18 +05:30
has_many :lfs_file_locks
2017-09-10 17:25:29 +05:30
has_many :project_group_links
2016-06-02 11:05:42 +05:30
has_many :invited_groups, through: :project_group_links, source: :group
2017-09-10 17:25:29 +05:30
has_many :pages_domains
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2018-05-09 12:01:36 +05:30
has_many :internal_ids
2017-09-10 17:25:29 +05:30
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
2018-03-17 18:26:18 +05:30
has_one :project_feature, inverse_of: :project
2017-09-10 17:25:29 +05:30
has_one :statistics, class_name: 'ProjectStatistics'
2018-03-17 18:26:18 +05:30
has_one :cluster_project, class_name: 'Clusters::Project'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
2018-11-08 19:23:39 +05:30
has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress'
2019-02-15 15:39:39 +05:30
has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
2018-03-17 18:26:18 +05:30
2018-11-20 20:47:30 +05:30
has_many :prometheus_metrics
2017-09-10 17:25:29 +05:30
# 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
2019-07-07 11:18:12 +05:30
# The relation :all_pipelines is intended to be used when we want to get the
2019-02-15 15:39:39 +05:30
# whole list of pipelines associated to the project
has_many :all_pipelines, class_name: 'Ci::Pipeline', inverse_of: :project
2019-07-07 11:18:12 +05:30
# The relation :ci_pipelines is intended to be used when we want to get only
2019-02-15 15:39:39 +05:30
# those pipeline which are directly related to CI. There are
# other pipelines, like webide ones, that we won't retrieve
# if we use this relation.
has_many :ci_pipelines,
-> { ci_sources },
class_name: 'Ci::Pipeline',
inverse_of: :project
2018-11-08 19:23:39 +05:30
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
2017-09-10 17:25:29 +05:30
# 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.
2018-03-17 18:26:18 +05:30
has_many :builds, class_name: 'Ci::Build', inverse_of: :project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :build_trace_section_names, class_name: 'Ci::BuildTraceSectionName'
2018-10-15 14:42:47 +05:30
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
2018-11-08 19:23:39 +05:30
has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
2015-12-23 02:04:40 +05:30
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
2017-09-10 17:25:29 +05:30
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
2018-12-13 13:39:08 +05:30
has_many :deployments, -> { success }
2017-09-10 17:25:29 +05:30
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
2018-05-09 12:01:36 +05:30
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
2018-03-27 19:54:05 +05:30
has_many :project_badges, class_name: 'ProjectBadge'
2018-10-15 14:42:47 +05:30
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
2018-03-27 19:54:05 +05:30
2015-12-23 02:04:40 +05:30
accepts_nested_attributes_for :variables, allow_destroy: true
2018-03-17 18:26:18 +05:30
accepts_nested_attributes_for :project_feature, update_only: true
2017-09-10 17:25:29 +05:30
accepts_nested_attributes_for :import_data
2018-03-17 18:26:18 +05:30
accepts_nested_attributes_for :auto_devops, update_only: true
2015-04-26 12:48:37 +05:30
2018-10-15 14:42:47 +05:30
accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
2019-02-15 15:39:39 +05:30
accepts_nested_attributes_for :error_tracking_setting, update_only: true
2014-09-02 18:07:02 +05:30
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
2017-08-17 22:00:37 +05:30
delegate :add_user, :add_users, to: :team
2018-11-18 11:00:15 +05:30
delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team
delegate :add_master, to: :team # @deprecated
2018-10-15 14:42:47 +05:30
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
2019-02-15 15:39:39 +05:30
delegate :group_clusters_enabled?, to: :group, allow_nil: true
delegate :root_ancestor, to: :namespace, allow_nil: true
2019-07-07 11:18:12 +05:30
delegate :last_pipeline, to: :commit, allow_nil: true
2014-09-02 18:07:02 +05:30
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
2017-09-10 17:25:29 +05:30
validates :ci_config_path,
2018-03-17 18:26:18 +05:30
format: { without: %r{(\.{2}|\A/)},
message: 'cannot include leading slash or directory traversal.' },
2017-09-10 17:25:29 +05:30
length: { maximum: 255 },
allow_blank: true
2015-04-26 12:48:37 +05:30
validates :name,
presence: true,
2017-08-17 22:00:37 +05:30
length: { maximum: 255 },
2015-04-26 12:48:37 +05:30
format: { with: Gitlab::Regex.project_name_regex,
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
2018-03-17 18:26:18 +05:30
project_path: true,
length: { maximum: 255 }
2017-08-17 22:00:37 +05:30
2014-09-02 18:07:02 +05:30
validates :namespace, presence: true
2017-08-17 22:00:37 +05:30
validates :name, uniqueness: { scope: :namespace_id }
2019-01-03 12:48:30 +05:30
validates :import_url, public_url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
enforce_user: true }, if: [:external_import?, :import_url_changed?]
2014-09-02 18:07:02 +05:30
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
2019-03-02 22:35:43 +05:30
validate :check_personal_projects_limit, on: :create
2018-03-17 18:26:18 +05:30
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
2019-06-05 12:25:43 +05:30
validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
2016-08-24 12:49:21 +05:30
validate :check_wiki_path_conflict
2018-05-09 12:01:36 +05:30
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
2016-08-24 12:49:21 +05:30
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
2018-03-17 18:26:18 +05:30
validates :variables, variable_duplicates: { scope: :environment_scope }
2019-02-15 15:39:39 +05:30
validates :bfg_object_map, file_size: { maximum: :max_attachment_size }
2015-04-26 12:48:37 +05:30
2014-09-02 18:07:02 +05:30
# Scopes
2017-09-10 17:25:29 +05:30
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
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) }
2018-03-27 19:54:05 +05:30
# 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") }
2018-12-05 23:21:45 +05:30
scope :sorted_by_stars, -> { reorder(star_count: :desc) }
2015-04-26 12:48:37 +05:30
2015-09-11 14:41:01 +05:30
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
2014-09-02 18:07:02 +05:30
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
2015-04-26 12:48:37 +05:30
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
2017-09-10 17:25:29 +05:30
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
2016-06-02 11:05:42 +05:30
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
2018-11-18 11:00:15 +05:30
scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) }
2018-03-17 18:26:18 +05:30
scope :archived, -> { where(archived: true) }
2014-09-02 18:07:02 +05:30
scope :non_archived, -> { where(archived: false) }
2016-06-02 11:05:42 +05:30
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
2016-11-24 13:41:30 +05:30
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
2017-08-17 22:00:37 +05:30
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.
2017-09-10 17:25:29 +05:30
joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'")
.where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%")
2017-08-17 22:00:37 +05:30
end
2016-11-24 13:41:30 +05:30
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
2019-02-15 15:39:39 +05:30
access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)]
enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil))
with_project_feature.where(enabled_feature)
2016-11-24 13:41:30 +05:30
}
# 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 })
}
2019-03-02 22:35:43 +05:30
# Picks projects which use the given programming language
scope :with_programming_language, ->(language_name) do
lang_id_query = ProgrammingLanguage
.with_name_case_insensitive(language_name)
.select(:id)
joins(:repository_languages)
.where(repository_languages: { programming_language_id: lang_id_query })
end
2016-11-24 13:41:30 +05:30
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
2018-03-17 18:26:18 +05:30
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
2019-06-05 12:25:43 +05:30
scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) }
2017-09-10 17:25:29 +05:30
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
2018-10-15 14:42:47 +05:30
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_group_runners_enabled, -> do
joins(:ci_cd_settings)
.where(project_ci_cd_settings: { group_runners_enabled: true })
end
2016-11-24 13:41:30 +05:30
2019-02-15 15:39:39 +05:30
scope :missing_kubernetes_namespace, -> (kubernetes_namespaces) do
subquery = kubernetes_namespaces.select('1').where('clusters_kubernetes_namespaces.project_id = projects.id')
where('NOT EXISTS (?)', subquery)
end
2017-08-17 22:00:37 +05:30
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
2019-02-15 15:39:39 +05:30
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
default: 3600, error_message: 'Maximum job timeout has a value which could not be accepted'
2018-05-09 12:01:36 +05:30
validates :build_timeout, allow_nil: true,
2018-11-18 11:00:15 +05:30
numericality: { greater_than_or_equal_to: 10.minutes,
less_than: 1.month,
only_integer: true,
message: 'needs to be beetween 10 minutes and 1 month' }
2018-05-09 12:01:36 +05:30
2019-02-15 15:39:39 +05:30
# Used by Projects::CleanupService to hold a map of rewritten object IDs
mount_uploader :bfg_object_map, AttachmentUploader
2018-12-13 13:39:08 +05:30
# Returns a project, if it is not about to be removed.
#
# id - The ID of the project to retrieve.
def self.find_without_deleted(id)
without_deleted.find_by_id(id)
end
2018-11-20 20:47:30 +05:30
# Paginates a collection using a `WHERE id < ?` condition.
#
# before - A project ID to use for filtering out projects with an equal or
# greater ID. If no ID is given, all projects are included.
#
# limit - The maximum number of rows to include.
def self.paginate_in_descending_order_using_id(
before: nil,
limit: Kaminari.config.default_per_page
)
relation = order_id_desc.limit(limit)
relation = relation.where('projects.id < ?', before) if before
relation
end
def self.eager_load_namespace_and_owner
includes(namespace: :owner)
end
2017-09-10 17:25:29 +05:30
# Returns a collection of projects that is either public or visible to the
# logged in user.
2019-07-07 11:18:12 +05:30
def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.admin?
2017-09-10 17:25:29 +05:30
if user
2018-03-17 18:26:18 +05:30
where('EXISTS (?) OR projects.visibility_level IN (?)',
2019-07-07 11:18:12 +05:30
user.authorizations_for_projects(min_access_level: min_access_level),
2018-03-17 18:26:18 +05:30
Gitlab::VisibilityLevel.levels_for_user(user))
2017-09-10 17:25:29 +05:30
else
public_to_user
end
end
2018-12-05 23:21:45 +05:30
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
2016-11-24 13:41:30 +05:30
# they are only available to team members. This scope returns projects where
2018-12-05 23:21:45 +05:30
# the feature is either public, enabled, or internal with permission for the user.
2019-07-07 11:18:12 +05:30
# Note: this scope doesn't enforce that the user has access to the projects, it just checks
# that the user has access to the feature. It's important to use this scope with others
# that checks project authorizations first.
2017-09-10 17:25:29 +05:30
#
# 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.
2016-11-24 13:41:30 +05:30
def self.with_feature_available_for_user(feature, user)
2019-02-15 15:39:39 +05:30
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
2016-11-24 13:41:30 +05:30
2017-09-10 17:25:29 +05:30
if user&.admin?
with_feature_enabled(feature)
elsif user
2019-07-07 11:18:12 +05:30
min_access_level = ProjectFeature.required_minimum_access_level(feature)
2017-09-10 17:25:29 +05:30
column = ProjectFeature.quoted_access_level_column(feature)
2016-11-24 13:41:30 +05:30
2017-09-10 17:25:29 +05:30
with_project_feature
2019-07-07 11:18:12 +05:30
.where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
{
public_visible: visible,
private_visible: ProjectFeature::PRIVATE,
authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
})
2017-09-10 17:25:29 +05:30
else
2019-07-07 11:18:12 +05:30
# This has to be added to include features whose value is nil in the db
visible << nil
2017-09-10 17:25:29 +05:30
with_feature_access_level(feature, visible)
end
2016-11-24 13:41:30 +05:30
end
2016-09-29 09:46:39 +05:30
2016-06-02 11:05:42 +05:30
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) }
2014-09-02 18:07:02 +05:30
2016-09-13 17:45:13 +05:30
scope :excluding_project, ->(project) { where.not(id: project) }
2017-09-10 17:25:29 +05:30
2019-02-15 15:39:39 +05:30
# We require an alias to the project_mirror_data_table in order to use import_state in our queries
scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
2018-12-13 13:39:08 +05:30
scope :for_group, -> (group) { where(group: group) }
2014-09-02 18:07:02 +05:30
class << self
2016-06-02 11:05:42 +05:30
# 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)
2018-03-17 18:26:18 +05:30
fuzzy_search(query, [:path, :name, :description])
2014-09-02 18:07:02 +05:30
end
2015-04-26 12:48:37 +05:30
def search_by_title(query)
2018-03-17 18:26:18 +05:30
non_archived.fuzzy_search(query, [:name])
2014-09-02 18:07:02 +05:30
end
def visibility_levels
Gitlab::VisibilityLevel.options
end
2018-05-09 12:01:36 +05:30
def sort_by_attribute(method)
2017-08-17 22:00:37 +05:30
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)
2018-12-05 23:21:45 +05:30
when 'stars_desc'
sorted_by_stars
2015-04-26 12:48:37 +05:30
else
order_by(method)
2014-09-02 18:07:02 +05:30
end
end
2015-09-11 14:41:01 +05:30
def reference_pattern
2017-08-17 22:00:37 +05:30
%r{
2019-02-02 18:00:53 +05:30
(?<!#{Gitlab::PathRegex::PATH_START_CHAR})
2017-09-10 17:25:29 +05:30
((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)?
(?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX})
2017-08-17 22:00:37 +05:30
}x
2015-09-11 14:41:01 +05:30
end
2015-10-24 18:46:33 +05:30
2018-11-20 20:47:30 +05:30
def reference_postfix
'>'
end
def reference_postfix_escaped
'&gt;'
end
# Pattern used to extract `namespace/project>` project references from text.
# '>' or its escaped form ('&gt;') are checked for because '>' is sometimes escaped
# when the reference comes from an external source.
def markdown_reference_pattern
%r{
#{reference_pattern}
(#{reference_postfix}|#{reference_postfix_escaped})
}x
end
2016-11-03 12:29:30 +05:30
def trending
2017-09-10 17:25:29 +05:30
joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id')
.reorder('trending_projects.id ASC')
2015-10-24 18:46:33 +05:30
end
2016-06-22 15:30:34 +05:30
2016-08-24 12:49:21 +05:30
def cached_count
Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
Project.count
end
2016-06-22 15:30:34 +05:30
end
2016-11-03 12:29:30 +05:30
def group_ids
2016-11-24 13:41:30 +05:30
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
2016-11-03 12:29:30 +05:30
end
2019-06-05 12:25:43 +05:30
# Returns ids of projects with milestones available for given user
#
# Used on queries to find milestones which user can see
# For example: Milestone.where(project_id: ids_with_milestone_available_for(user))
def ids_with_milestone_available_for(user)
with_issues_enabled = with_issues_available_for_user(user).select(:id)
with_merge_requests_enabled = with_merge_requests_available_for_user(user).select(:id)
from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
end
2014-09-02 18:07:02 +05:30
end
2019-02-15 15:39:39 +05:30
def all_pipelines
2019-02-02 18:00:53 +05:30
if builds_enabled?
super
else
super.external
end
end
2019-07-07 11:18:12 +05:30
def ci_pipelines
if builds_enabled?
super
else
super.external
end
end
2018-03-17 18:26:18 +05:30
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
2019-02-15 15:39:39 +05:30
def ancestors_upto(top = nil, hierarchy_order: nil)
Gitlab::ObjectHierarchy.new(Group.where(id: namespace_id))
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
2018-03-17 18:26:18 +05:30
end
2019-02-15 15:39:39 +05:30
alias_method :ancestors, :ancestors_upto
2016-09-29 09:46:39 +05:30
def lfs_enabled?
return namespace.lfs_enabled? if self[:lfs_enabled].nil?
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
2018-12-13 13:39:08 +05:30
alias_method :lfs_enabled, :lfs_enabled?
2018-03-17 18:26:18 +05:30
def auto_devops_enabled?
if auto_devops&.enabled.nil?
2018-11-20 20:47:30 +05:30
has_auto_devops_implicitly_enabled?
2018-03-17 18:26:18 +05:30
else
auto_devops.enabled?
end
end
2018-11-18 11:00:15 +05:30
def has_auto_devops_implicitly_enabled?
2019-07-07 11:18:12 +05:30
auto_devops_config = first_auto_devops_config
auto_devops_config[:scope] != :project && auto_devops_config[:status]
2018-11-18 11:00:15 +05:30
end
2018-03-17 18:26:18 +05:30
def has_auto_devops_implicitly_disabled?
2019-07-07 11:18:12 +05:30
auto_devops_config = first_auto_devops_config
auto_devops_config[:scope] != :project && !auto_devops_config[:status]
end
def first_auto_devops_config
return namespace.first_auto_devops_config if auto_devops&.enabled.nil?
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
2018-03-17 18:26:18 +05:30
end
def empty_repo?
repository.empty?
end
2014-09-02 18:07:02 +05:30
def team
@team ||= ProjectTeam.new(self)
end
def repository
2017-09-10 17:25:29 +05:30
@repository ||= Repository.new(full_path, self, disk_path: disk_path)
2015-09-11 14:41:01 +05:30
end
2018-03-17 18:26:18 +05:30
def cleanup
@repository = nil
end
alias_method :reload_repository!, :cleanup
2017-08-17 22:00:37 +05:30
def container_registry_url
2016-06-02 11:05:42 +05:30
if Gitlab.config.registry.enabled
2017-09-10 17:25:29 +05:30
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
2016-06-02 11:05:42 +05:30
end
end
def has_container_registry_tags?
2017-09-10 17:25:29 +05:30
return @images if defined?(@images)
@images = container_repositories.to_a.any?(&:has_tags?) ||
2017-08-17 22:00:37 +05:30
has_root_container_repository_tags?
2016-06-02 11:05:42 +05:30
end
2016-08-24 12:49:21 +05:30
def commit(ref = 'HEAD')
repository.commit(ref)
end
2018-03-17 18:26:18 +05:30
def commit_by(oid:)
repository.commit_by(oid: oid)
end
2018-11-18 11:00:15 +05:30
def commits_by(oids:)
repository.commits_by(oids: oids)
end
2016-08-24 12:49:21 +05:30
# ref can't be HEAD, can only be branch/tag name or SHA
2019-03-02 22:35:43 +05:30
def latest_successful_build_for(job_name, ref = default_branch)
2019-02-15 15:39:39 +05:30
latest_pipeline = ci_pipelines.latest_successful_for(ref)
2019-03-02 22:35:43 +05:30
return unless latest_pipeline
2016-08-24 12:49:21 +05:30
2019-03-02 22:35:43 +05:30
latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
2014-09-02 18:07:02 +05:30
end
2019-03-02 22:35:43 +05:30
def latest_successful_build_for!(job_name, ref = default_branch)
latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}"))
2019-02-15 15:39:39 +05:30
end
2016-02-05 20:25:01 +05:30
def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id)
2018-03-17 18:26:18 +05:30
commit_by(oid: sha) if sha
2016-02-05 20:25:01 +05:30
end
2014-09-02 18:07:02 +05:30
def saved?
id && persisted?
end
2019-02-15 15:39:39 +05:30
def import_status
import_state&.status || 'none'
end
def human_import_status_name
import_state&.human_status_name || 'none'
end
2014-09-02 18:07:02 +05:30
def add_import_job
2017-08-17 22:00:37 +05:30
job_id =
if forked?
2018-05-09 12:01:36 +05:30
RepositoryForkWorker.perform_async(id)
2018-03-17 18:26:18 +05:30
elsif gitlab_project_import?
# Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
RepositoryImportWorker.set(retry: false).perform_async(self.id)
2017-08-17 22:00:37 +05:30
else
RepositoryImportWorker.perform_async(self.id)
end
2016-06-02 11:05:42 +05:30
2018-03-17 18:26:18 +05:30
log_import_activity(job_id)
job_id
end
def log_import_activity(job_id, type: :import)
job_type = type.to_s.capitalize
2016-06-02 11:05:42 +05:30
if job_id
2018-03-17 18:26:18 +05:30
Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
2015-09-25 12:07:36 +05:30
else
2018-03-17 18:26:18 +05:30
Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
2015-09-25 12:07:36 +05:30
end
2014-09-02 18:07:02 +05:30
end
2016-06-02 11:05:42 +05:30
def reset_cache_and_import_attrs
2017-09-10 17:25:29 +05:30
run_after_commit do
ProjectCacheWorker.perform_async(self.id)
end
2019-02-15 15:39:39 +05:30
import_state.update(last_error: nil)
2017-09-10 17:25:29 +05:30
remove_import_data
end
2015-11-26 14:37:03 +05:30
2018-12-13 13:39:08 +05:30
# This method is overridden in EE::Project model
2017-09-10 17:25:29 +05:30
def remove_import_data
import_data&.destroy
end
def ci_config_path=(value)
# Strip all leading slashes so that //foo -> foo
2018-03-17 18:26:18 +05:30
super(value&.delete("\0"))
2015-04-26 12:48:37 +05:30
end
2016-06-02 11:05:42 +05:30
def import_url=(value)
2019-03-02 22:35:43 +05:30
if Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
super(import_url.sanitized_url)
create_or_update_import_data(credentials: import_url.credentials)
else
super(value)
end
2016-06-02 11:05:42 +05:30
end
def import_url
2016-11-03 12:29:30 +05:30
if import_data && super.present?
2016-06-02 11:05:42 +05:30
import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
import_url.full_url
else
super
end
2018-12-13 13:39:08 +05:30
rescue
super
2016-06-02 11:05:42 +05:30
end
2016-08-24 12:49:21 +05:30
def valid_import_url?
2017-09-10 17:25:29 +05:30
valid?(:import_url) || errors.messages[:import_url].nil?
2016-08-24 12:49:21 +05:30
end
2016-06-02 11:05:42 +05:30
def create_or_update_import_data(data: nil, credentials: nil)
2018-05-09 12:01:36 +05:30
return if data.nil? && credentials.nil?
2016-08-24 12:49:21 +05:30
2016-06-02 11:05:42 +05:30
project_import_data = import_data || build_import_data
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
project_import_data.merge_data(data.to_h)
project_import_data.merge_credentials(credentials.to_h)
2018-11-18 11:00:15 +05:30
project_import_data
2016-06-02 11:05:42 +05:30
end
2014-09-02 18:07:02 +05:30
def import?
2018-03-17 18:26:18 +05:30
external_import? || forked? || gitlab_project_import? || bare_repository_import?
2015-09-25 12:07:36 +05:30
end
def external_import?
2014-09-02 18:07:02 +05:30
import_url.present?
end
2015-11-26 14:37:03 +05:30
def safe_import_url
2016-06-02 11:05:42 +05:30
Gitlab::UrlSanitizer.new(import_url).masked_url
2015-11-26 14:37:03 +05:30
end
2018-03-17 18:26:18 +05:30
def bare_repository_import?
import_type == 'bare_repository'
end
2016-06-22 15:30:34 +05:30
def gitlab_project_import?
import_type == 'gitlab_project'
end
2017-08-17 22:00:37 +05:30
def gitea_import?
import_type == 'gitea'
end
2018-10-15 14:42:47 +05:30
def has_remote_mirror?
remote_mirror_available? && remote_mirrors.enabled.exists?
end
def updating_remote_mirror?
remote_mirrors.enabled.started.exists?
end
def update_remote_mirrors
return unless remote_mirror_available?
remote_mirrors.enabled.each(&:sync)
end
def mark_stuck_remote_mirrors_as_failed!
remote_mirrors.stuck.update_all(
update_status: :failed,
last_error: 'The remote mirror took to long to complete.',
last_update_at: Time.now
)
end
def mark_remote_mirrors_for_removal
remote_mirrors.each(&:mark_for_delete_if_blank_url)
end
def remote_mirror_available?
remote_mirror_available_overridden ||
::Gitlab::CurrentSettings.mirror_available
end
2019-03-02 22:35:43 +05:30
def check_personal_projects_limit
# Since this method is called as validation hook, `creator` might not be
# present. Since the validation for that will fail, we can just return
# early.
return if !creator || creator.can_create_project? ||
namespace.kind == 'group'
2019-03-02 22:35:43 +05:30
limit = creator.projects_limit
error =
if limit.zero?
_('Personal project creation is not allowed. Please contact your administrator with questions')
else
2019-03-02 22:35:43 +05:30
_('Your project limit is %{limit} projects! Please contact your administrator to increase it')
end
2019-03-02 22:35:43 +05:30
self.errors.add(:limit_reached, error % { limit: limit })
2016-06-02 11:05:42 +05:30
end
2019-06-05 12:25:43 +05:30
def should_validate_visibility_level?
new_record? || changes.has_key?(:visibility_level)
end
2016-06-02 11:05:42 +05:30
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.")
2014-09-02 18:07:02 +05:30
end
2016-08-24 12:49:21 +05:30
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
2018-05-09 12:01:36 +05:30
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
2014-09-02 18:07:02 +05:30
def to_param
2016-09-13 17:45:13 +05:30
if persisted? && errors.include?(:path)
path_was
else
path
end
2014-09-02 18:07:02 +05:30
end
2018-11-20 20:47:30 +05:30
def to_reference_with_postfix
"#{to_reference(full: true)}#{self.class.reference_postfix}"
end
2017-08-17 22:00:37 +05:30
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
if full || cross_namespace_reference?(from)
2017-09-10 17:25:29 +05:30
full_path
2017-08-17 22:00:37 +05:30
elsif cross_project_reference?(from)
path
end
2015-09-11 14:41:01 +05:30
end
2018-03-17 18:26:18 +05:30
def to_human_reference(from = nil)
if cross_namespace_reference?(from)
2017-08-17 22:00:37 +05:30
name_with_namespace
2018-03-17 18:26:18 +05:30
elsif cross_project_reference?(from)
2017-08-17 22:00:37 +05:30
name
end
2014-09-02 18:07:02 +05:30
end
2017-08-17 22:00:37 +05:30
def web_url
2017-09-10 17:25:29 +05:30
Gitlab::Routing.url_helpers.project_url(self)
2014-09-02 18:07:02 +05:30
end
2018-11-08 19:23:39 +05:30
def readme_url
2019-02-15 15:39:39 +05:30
readme_path = repository.readme_path
if readme_path
Gitlab::Routing.url_helpers.project_blob_url(self, File.join(default_branch, readme_path))
2018-11-08 19:23:39 +05:30
end
end
2018-03-17 18:26:18 +05:30
def new_issuable_address(author, address_type)
2017-08-17 22:00:37 +05:30
return unless Gitlab::IncomingEmail.supports_issue_creation? && author
2016-09-13 17:45:13 +05:30
2019-02-15 15:39:39 +05:30
# check since this can come from a request parameter
return unless %w(issue merge_request).include?(address_type)
2017-08-17 22:00:37 +05:30
author.ensure_incoming_email_token!
2019-02-15 15:39:39 +05:30
suffix = address_type.dasherize
# example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-issue@localhost.com
# example: incoming+h5bp-html5-boilerplate-8-1234567890abcdef123456789-merge-request@localhost.com
Gitlab::IncomingEmail.reply_address("#{full_path_slug}-#{project_id}-#{author.incoming_email_token}-#{suffix}")
2016-09-13 17:45:13 +05:30
end
2014-09-02 18:07:02 +05:30
def build_commit_note(commit)
2015-04-26 12:48:37 +05:30
notes.new(commit_id: commit.id, noteable_type: 'Commit')
2014-09-02 18:07:02 +05:30
end
def last_activity
last_event
end
def last_activity_date
2018-03-27 19:54:05 +05:30
[last_activity_at, last_repository_updated_at, updated_at].compact.max
2014-09-02 18:07:02 +05:30
end
def project_id
self.id
end
2017-01-15 13:20:01 +05:30
def get_issue(issue_id, current_user)
2017-09-10 17:25:29 +05:30
issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?
if issue
issue
elsif external_issue_tracker
2015-09-11 14:41:01 +05:30
ExternalIssue.new(issue_id, self)
2014-09-02 18:07:02 +05:30
end
end
2015-09-11 14:41:01 +05:30
def issue_exists?(issue_id)
get_issue(issue_id)
end
2015-04-26 12:48:37 +05:30
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
2017-08-17 22:00:37 +05:30
def external_issue_reference_pattern
2017-09-10 17:25:29 +05:30
external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
2016-11-03 12:29:30 +05:30
end
2015-04-26 12:48:37 +05:30
def default_issues_tracker?
2015-09-11 14:41:01 +05:30
!external_issue_tracker
2015-04-26 12:48:37 +05:30
end
def external_issue_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
2014-09-02 18:07:02 +05:30
end
def cache_has_external_issue_tracker
2018-03-17 18:26:18 +05:30
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?) if Gitlab::Database.read_write?
2014-09-02 18:07:02 +05:30
end
2016-09-29 09:46:39 +05:30
def has_wiki?
wiki_enabled? || has_external_wiki?
end
2016-08-24 12:49:21 +05:30
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
2018-03-17 18:26:18 +05:30
update_column(:has_external_wiki, services.external_wikis.any?) if Gitlab::Database.read_write?
2016-08-24 12:49:21 +05:30
end
2017-09-10 17:25:29 +05:30
def find_or_initialize_services(exceptions: [])
available_services_names = Service.available_services_names - exceptions
2018-11-08 19:23:39 +05:30
available_services = available_services_names.map do |service_name|
2018-12-05 23:21:45 +05:30
find_or_initialize_service(service_name)
2014-09-02 18:07:02 +05:30
end
2018-11-08 19:23:39 +05:30
2018-12-05 23:21:45 +05:30
available_services.compact
2018-11-08 19:23:39 +05:30
end
def disabled_services
[]
2014-09-02 18:07:02 +05:30
end
2017-08-17 22:00:37 +05:30
def find_or_initialize_service(name)
2018-12-05 23:21:45 +05:30
return if disabled_services.include?(name)
service = find_service(services, name)
return service if service
# We should check if template for the service exists
template = find_service(services_templates, name)
if template
Service.build_from_template(id, template)
else
# If no template, we should create an instance. Ex `build_gitlab_ci_service`
public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2015-09-25 12:07:36 +05:30
def create_labels
Label.templates.each do |label|
2019-03-02 22:35:43 +05:30
params = label.attributes.except('id', 'template', 'created_at', 'updated_at', 'type')
2016-11-03 12:29:30 +05:30
Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
2015-09-25 12:07:36 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2015-09-25 12:07:36 +05:30
2015-04-26 12:48:37 +05:30
def find_service(list, name)
list.find { |service| service.to_param == name }
2014-09-02 18:07:02 +05:30
end
def ci_services
2016-06-02 11:05:42 +05:30
services.where(category: :ci)
2014-09-02 18:07:02 +05:30
end
def ci_service
2016-06-02 11:05:42 +05:30
@ci_service ||= ci_services.reorder(nil).find_by(active: true)
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def monitoring_services
services.where(category: :monitoring)
end
def monitoring_service
@monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
end
2015-12-23 02:04:40 +05:30
def jira_tracker?
issues_tracker.to_param == 'jira'
2014-09-02 18:07:02 +05:30
end
2015-04-26 12:48:37 +05:30
def avatar_in_git
2016-06-02 11:05:42 +05:30
repository.avatar
2015-04-26 12:48:37 +05:30
end
2017-09-10 17:25:29 +05:30
def avatar_url(**args)
2018-03-17 18:26:18 +05:30
Gitlab::Routing.url_helpers.project_avatar_url(self) if avatar_in_git
2015-04-26 12:48:37 +05:30
end
2014-09-02 18:07:02 +05:30
# For compatibility with old code
def code
path
end
2019-02-15 15:39:39 +05:30
def all_clusters
group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )
Clusters::Cluster.from_union([clusters, group_clusters])
end
2015-04-26 12:48:37 +05:30
def items_for(entity)
2014-09-02 18:07:02 +05:30
case entity
when 'issue' then
issues
when 'merge_request' then
merge_requests
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2015-10-24 18:46:33 +05:30
def send_move_instructions(old_path_with_namespace)
# New project path needs to be committed to the DB or notification will
# retrieve stale information
2018-03-17 18:26:18 +05:30
run_after_commit do
NotificationService.new.project_was_moved(self, old_path_with_namespace)
end
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2014-09-02 18:07:02 +05:30
def owner
if group
group
else
namespace.try(:owner)
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2014-09-02 18:07:02 +05:30
def execute_hooks(data, hooks_scope = :push_hooks)
2018-03-17 18:26:18 +05:30
run_after_commit_or_now do
2018-11-20 20:47:30 +05:30
hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook|
2018-03-17 18:26:18 +05:30
hook.async_execute(data, hooks_scope.to_s)
end
SystemHooksService.new.execute_hooks(data, hooks_scope)
2014-09-02 18:07:02 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2014-09-02 18:07:02 +05:30
2015-04-26 12:48:37 +05:30
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
2018-03-17 18:26:18 +05:30
run_after_commit_or_now do
services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
service.async_execute(data)
end
2014-09-02 18:07:02 +05:30
end
end
def valid_repo?
repository.exists?
rescue
2015-04-26 12:48:37 +05:30
errors.add(:path, 'Invalid repository path')
2014-09-02 18:07:02 +05:30
false
end
def url_to_repo
2017-09-10 17:25:29 +05:30
gitlab_shell.url_to_repo(full_path)
2014-09-02 18:07:02 +05:30
end
def repo_exists?
2018-03-17 18:26:18 +05:30
strong_memoize(:repo_exists) do
2019-07-07 11:18:12 +05:30
repository.exists?
rescue
false
2018-03-17 18:26:18 +05:30
end
2014-09-02 18:07:02 +05:30
end
def root_ref?(branch)
repository.root_ref == branch
end
def ssh_url_to_repo
url_to_repo
end
2017-09-10 17:25:29 +05:30
def http_url_to_repo
"#{web_url}.git"
2014-09-02 18:07:02 +05:30
end
2019-07-07 11:18:12 +05:30
# Is overridden in EE
2019-02-15 15:39:39 +05:30
def lfs_http_url_to_repo(_)
http_url_to_repo
end
2014-09-02 18:07:02 +05:30
def forked?
2018-12-13 13:39:08 +05:30
fork_network && fork_network.root_project != self
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def fork_source
2019-07-07 11:18:12 +05:30
return unless forked?
2018-03-17 18:26:18 +05:30
forked_from_project || fork_network&.root_project
end
def lfs_storage_project
@lfs_storage_project ||= begin
result = self
# TODO: Make this go to the fork_network root immeadiatly
# dependant on the discussion in: https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
result = result.fork_source while result&.forked?
result || self
end
end
2018-05-09 12:01:36 +05:30
# This will return all `lfs_objects` that are accessible to the project.
# So this might be `self.lfs_objects` if the project is not part of a fork
# network, or it is the base of the fork network.
#
# TODO: refactor this to get the correct lfs objects when implementing
# https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
def all_lfs_objects
lfs_storage_project.lfs_objects
end
2014-09-02 18:07:02 +05:30
def personal?
!group
end
2016-04-02 18:10:28 +05:30
# 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?
2016-06-02 11:05:42 +05:30
repo.before_delete
2016-04-02 18:10:28 +05:30
end
if wiki.exists?
2016-06-02 11:05:42 +05:30
wiki.before_delete
2016-04-02 18:10:28 +05:30
end
end
2017-09-10 17:25:29 +05:30
# Check if repository already exists on disk
2018-03-17 18:26:18 +05:30
def check_repository_path_availability
return true if skip_disk_validation
2018-10-15 14:42:47 +05:30
return false unless repository_storage
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
# Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists?
2017-09-10 17:25:29 +05:30
errors.add(:base, 'There is already a repository with that name on disk')
return false
end
true
2018-03-17 18:26:18 +05:30
rescue GRPC::Internal # if the path is too long
false
2017-09-10 17:25:29 +05:30
end
2019-02-15 15:39:39 +05:30
def track_project_repository
repository = project_repository || build_project_repository
repository.update!(shard_name: repository_storage, disk_path: disk_path)
end
2018-03-17 18:26:18 +05:30
def create_repository(force: false)
# Forked import is handled asynchronously
return if forked? && !force
2019-03-02 22:35:43 +05:30
if gitlab_shell.create_project_repository(self)
2018-03-17 18:26:18 +05:30
repository.after_create
true
else
errors.add(:base, 'Failed to create repository via gitlab-shell')
false
end
2017-09-10 17:25:29 +05:30
end
2016-06-02 11:05:42 +05:30
def hook_attrs(backward: true)
attrs = {
2018-03-17 18:26:18 +05:30
id: id,
2015-04-26 12:48:37 +05:30
name: name,
2016-04-02 18:10:28 +05:30
description: description,
2015-09-25 12:07:36 +05:30
web_url: web_url,
2018-03-17 18:26:18 +05:30
avatar_url: avatar_url(only_path: false),
2016-04-02 18:10:28 +05:30
git_ssh_url: ssh_url_to_repo,
git_http_url: http_url_to_repo,
2015-04-26 12:48:37 +05:30
namespace: namespace.name,
2016-04-02 18:10:28 +05:30
visibility_level: visibility_level,
2017-09-10 17:25:29 +05:30
path_with_namespace: full_path,
2016-04-02 18:10:28 +05:30
default_branch: default_branch,
2017-09-10 17:25:29 +05:30
ci_config_path: ci_config_path
2015-04-26 12:48:37 +05:30
}
2016-06-02 11:05:42 +05:30
# Backward compatibility
if backward
attrs.merge!({
homepage: web_url,
url: url_to_repo,
ssh_url: ssh_url_to_repo,
http_url: http_url_to_repo
})
end
attrs
2015-04-26 12:48:37 +05:30
end
2014-09-02 18:07:02 +05:30
def project_member(user)
2018-03-17 18:26:18 +05:30
if project_members.loaded?
project_members.find { |member| member.user_id == user.id }
else
project_members.find_by(user_id: user)
end
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
# Filters `users` to return only authorized users of the project
def members_among(users)
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
authorized_users.merge(users)
else
return [] if users.empty?
user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id)
users.select { |user| user_ids.include?(user.id) }
end
end
2014-09-02 18:07:02 +05:30
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
def reload_default_branch
@default_branch = nil
default_branch
end
def visibility_level_field
2017-08-17 22:00:37 +05:30
:visibility_level
2014-09-02 18:07:02 +05:30
end
def change_head(branch)
2018-03-17 18:26:18 +05:30
if repository.branch_exists?(branch)
repository.before_change_head
2019-02-15 15:39:39 +05:30
repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}")
2018-03-17 18:26:18 +05:30
repository.copy_gitattributes(branch)
repository.after_change_head
2019-07-07 11:18:12 +05:30
ProjectCacheWorker.perform_async(self.id, [], [:commit_count])
2018-03-17 18:26:18 +05:30
reload_default_branch
else
errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist")
false
end
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def forked_from?(other_project)
forked? && forked_from_project == other_project
end
def in_fork_network_of?(other_project)
# TODO: Remove this in a next release when all fork_networks are populated
# This makes sure all MergeRequests remain valid while the projects don't
# have a fork_network yet.
return true if forked_from?(other_project)
return false if fork_network.nil? || other_project.fork_network.nil?
fork_network == other_project.fork_network
2014-09-02 18:07:02 +05:30
end
2015-04-26 12:48:37 +05:30
def origin_merge_requests
merge_requests.where(source_project_id: self.id)
end
2017-09-10 17:25:29 +05:30
def ensure_repository
create_repository(force: true) unless repository_exists?
2015-04-26 12:48:37 +05:30
end
def repository_exists?
!!repository.exists?
end
2018-03-17 18:26:18 +05:30
def wiki_repository_exists?
wiki.repository_exists?
end
# update visibility_level of forks
2017-09-10 17:25:29 +05:30
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
forks.each do |forked_project|
if forked_project.visibility_level > visibility_level
forked_project.visibility_level = visibility_level
forked_project.save!
end
end
end
2015-04-26 12:48:37 +05:30
def create_wiki
ProjectWiki.new(self, self.owner).wiki
true
2015-10-24 18:46:33 +05:30
rescue ProjectWiki::CouldNotCreateWikiError
2015-04-26 12:48:37 +05:30
errors.add(:base, 'Failed create wiki')
false
end
2015-10-24 18:46:33 +05:30
2017-09-10 17:25:29 +05:30
def wiki
@wiki ||= ProjectWiki.new(self, self.owner)
end
2015-12-23 02:04:40 +05:30
def jira_tracker_active?
jira_tracker? && jira_service.active
end
2016-06-02 11:05:42 +05:30
def allowed_to_share_with_group?
!namespace.share_with_group_lock
2015-10-24 18:46:33 +05:30
end
2016-09-29 09:46:39 +05:30
def pipeline_for(ref, sha = nil)
sha ||= commit(ref).try(:sha)
return unless sha
2019-02-15 15:39:39 +05:30
ci_pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
2015-10-24 18:46:33 +05:30
end
2018-03-17 18:26:18 +05:30
def latest_successful_pipeline_for_default_branch
if defined?(@latest_successful_pipeline_for_default_branch)
return @latest_successful_pipeline_for_default_branch
end
@latest_successful_pipeline_for_default_branch =
2019-02-15 15:39:39 +05:30
ci_pipelines.latest_successful_for(default_branch)
2018-03-17 18:26:18 +05:30
end
def latest_successful_pipeline_for(ref = nil)
if ref && ref != default_branch
2019-02-15 15:39:39 +05:30
ci_pipelines.latest_successful_for(ref)
2018-03-17 18:26:18 +05:30
else
latest_successful_pipeline_for_default_branch
end
end
2016-06-02 11:05:42 +05:30
def enable_ci
2016-09-29 09:46:39 +05:30
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
2015-11-26 14:37:03 +05:30
end
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
def shared_runners_available?
shared_runners_enabled?
end
2015-12-23 02:04:40 +05:30
2017-08-17 22:00:37 +05:30
def shared_runners
2018-11-08 19:23:39 +05:30
@shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
2015-12-23 02:04:40 +05:30
end
2018-10-15 14:42:47 +05:30
def group_runners
@group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none
end
def all_runners
2018-12-05 23:21:45 +05:30
Ci::Runner.from_union([runners, group_runners, shared_runners])
2015-12-23 02:04:40 +05:30
end
2018-11-08 19:23:39 +05:30
def active_runners
strong_memoize(:active_runners) do
all_runners.active
end
end
2017-08-17 22:00:37 +05:30
def any_runners?(&block)
2018-11-08 19:23:39 +05:30
active_runners.any?(&block)
2017-08-17 22:00:37 +05:30
end
def valid_runners_token?(token)
self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token)
2015-12-23 02:04:40 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-11-08 19:23:39 +05:30
def open_issues_count(current_user = nil)
Projects::OpenIssuesCountService.new(self, current_user).count
2018-03-17 18:26:18 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def open_merge_requests_count
Projects::OpenMergeRequestsCountService.new(self).count
2015-12-23 02:04:40 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2016-06-02 11:05:42 +05:30
def visibility_level_allowed_as_fork?(level = self.visibility_level)
return true unless forked?
2016-06-02 11:05:42 +05:30
2018-12-13 13:39:08 +05:30
original_project = fork_source
2016-06-02 11:05:42 +05:30
return true unless original_project
level <= original_project.visibility_level
end
def visibility_level_allowed_by_group?(level = self.visibility_level)
return true unless group
level <= group.visibility_level
end
def visibility_level_allowed?(level = self.visibility_level)
visibility_level_allowed_as_fork?(level) && visibility_level_allowed_by_group?(level)
end
def runners_token
ensure_runners_token!
end
2016-04-02 18:10:28 +05:30
2017-08-17 22:00:37 +05:30
def pages_deployed?
Dir.exist?(public_pages_path)
end
2018-05-09 12:01:36 +05:30
def pages_group_url
2017-08-17 22:00:37 +05:30
# The host in URL always needs to be downcased
2018-05-09 12:01:36 +05:30
Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{pages_subdomain}."
2017-08-17 22:00:37 +05:30
end.downcase
2018-05-09 12:01:36 +05:30
end
def pages_url
url = pages_group_url
url_path = full_path.partition('/').last
2017-08-17 22:00:37 +05:30
# If the project path is the same as host, we serve it as group page
2018-05-09 12:01:36 +05:30
return url if url == "#{Settings.pages.protocol}://#{url_path}"
2017-08-17 22:00:37 +05:30
"#{url}/#{url_path}"
end
def pages_subdomain
full_path.partition('/').first
end
def pages_path
2018-03-17 18:26:18 +05:30
# TODO: when we migrate Pages to work with new storage types, change here to use disk_path
File.join(Settings.pages.path, full_path)
2017-08-17 22:00:37 +05:30
end
def public_pages_path
File.join(pages_path, 'public')
end
2018-03-17 18:26:18 +05:30
def pages_available?
2019-03-02 22:35:43 +05:30
Gitlab.config.pages.enabled
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
def remove_private_deploy_keys
exclude_keys_linked_to_other_projects = <<-SQL
NOT EXISTS (
SELECT 1
FROM deploy_keys_projects dkp2
WHERE dkp2.deploy_key_id = deploy_keys_projects.deploy_key_id
AND dkp2.project_id != deploy_keys_projects.project_id
)
SQL
deploy_keys.where(public: false)
.where(exclude_keys_linked_to_other_projects)
.delete_all
end
# TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
def remove_pages
2017-09-10 17:25:29 +05:30
# Projects with a missing namespace cannot have their pages removed
return unless namespace
::Projects::UpdatePagesConfigurationService.new(self).execute
2017-08-17 22:00:37 +05:30
# 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching
# 3. We asynchronously remove pages with force
temp_path = "#{path}.#{SecureRandom.hex}.deleted"
if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.full_path)
PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path)
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
def write_repository_config(gl_full_path: full_path)
# We'd need to keep track of project full path otherwise directory tree
# created with hashed storage enabled cannot be usefully imported using
# the import rake task.
repository.raw_repository.write_config(full_path: gl_full_path)
rescue Gitlab::Git::Repository::NoRepository => e
Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.")
nil
end
def after_import
repository.after_import
2018-11-08 19:23:39 +05:30
wiki.repository.after_import
2019-03-02 22:35:43 +05:30
# The import assigns iid values on its own, e.g. by re-using GitHub ids.
# Flush existing InternalId records for this project for consistency reasons.
# Those records are going to be recreated with the next normal creation
# of a model instance (e.g. an Issue).
InternalId.flush_records!(project: self)
2019-02-15 15:39:39 +05:30
import_state.finish
import_state.remove_jid
2018-03-17 18:26:18 +05:30
update_project_counter_caches
after_create_default_branch
2019-02-15 15:39:39 +05:30
join_pool_repository
2018-05-09 12:01:36 +05:30
refresh_markdown_cache!
2018-03-17 18:26:18 +05:30
end
def update_project_counter_caches
classes = [
Projects::OpenIssuesCountService,
Projects::OpenMergeRequestsCountService
]
classes.each do |klass|
klass.new(self).refresh_cache
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def after_create_default_branch
2019-03-02 22:35:43 +05:30
Projects::ProtectDefaultBranchService.new(self).execute
2018-03-17 18:26:18 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
2017-08-17 22:00:37 +05:30
# Lazy loading of the `pipeline_status` attribute
def pipeline_status
@pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
2018-05-09 12:01:36 +05:30
def add_export_job(current_user:, after_export_strategy: nil, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
2016-06-22 15:30:34 +05:30
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
else
Rails.logger.error "Export job failed to start for project ID #{self.id}"
end
end
2018-03-27 19:54:05 +05:30
def import_export_shared
@import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
end
2016-06-22 15:30:34 +05:30
def export_path
2019-07-07 11:18:12 +05:30
return unless namespace.present? || hashed_storage?(:repository)
2018-03-17 18:26:18 +05:30
2018-03-27 19:54:05 +05:30
import_export_shared.archive_path
2016-06-22 15:30:34 +05:30
end
2018-03-27 19:54:05 +05:30
def export_status
if export_in_progress?
:started
2018-05-09 12:01:36 +05:30
elsif after_export_in_progress?
:after_export_action
2018-11-20 20:47:30 +05:30
elsif export_file_exists?
2018-03-27 19:54:05 +05:30
:finished
else
:none
end
end
def export_in_progress?
import_export_shared.active_export_count > 0
end
2018-05-09 12:01:36 +05:30
def after_export_in_progress?
import_export_shared.after_export_in_progress?
end
2018-11-20 20:47:30 +05:30
def remove_exports
return unless export_file_exists?
import_export_upload.remove_export_file!
import_export_upload.save unless import_export_upload.destroyed?
2018-03-17 18:26:18 +05:30
end
2018-11-20 20:47:30 +05:30
def export_file_exists?
export_file&.file
2018-11-08 19:23:39 +05:30
end
2018-05-09 12:01:36 +05:30
2018-11-20 20:47:30 +05:30
def export_file
import_export_upload&.export_file
2018-05-09 12:01:36 +05:30
end
2018-03-17 18:26:18 +05:30
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
def has_ci?
repository.gitlab_ci_yml || auto_devops_enabled?
2016-06-22 15:30:34 +05:30
end
2016-08-24 12:49:21 +05:30
def predefined_variables
2018-05-09 12:01:36 +05:30
visibility = Gitlab::VisibilityLevel.string_level(visibility_level)
Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PROJECT_ID', value: id.to_s)
.append(key: 'CI_PROJECT_NAME', value: path)
.append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
2019-03-02 22:35:43 +05:30
.concat(pages_variables)
2018-05-09 12:01:36 +05:30
.concat(container_registry_variables)
.concat(auto_devops_variables)
2019-02-15 15:39:39 +05:30
.concat(api_variables)
end
2019-03-02 22:35:43 +05:30
def pages_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host)
variables.append(key: 'CI_PAGES_URL', value: pages_url)
end
end
2019-02-15 15:39:39 +05:30
def api_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url)
end
2016-08-24 12:49:21 +05:30
end
def container_registry_variables
2018-05-09 12:01:36 +05:30
Gitlab::Ci::Variables::Collection.new.tap do |variables|
2018-10-15 14:42:47 +05:30
break variables unless Gitlab.config.registry.enabled
2016-08-24 12:49:21 +05:30
2018-05-09 12:01:36 +05:30
variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
2016-08-24 12:49:21 +05:30
2018-05-09 12:01:36 +05:30
if container_registry_enabled?
variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url)
end
2016-08-24 12:49:21 +05:30
end
end
2018-11-08 19:23:39 +05:30
def default_environment
production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
environments
.with_state(:available)
.reorder(production_first)
.first
end
2018-12-13 13:39:08 +05:30
def ci_variables_for(ref:, environment: nil)
2017-09-10 17:25:29 +05:30
# EE would use the environment
if protected_for?(ref)
variables
else
variables.unprotected
2016-08-24 12:49:21 +05:30
end
end
2017-09-10 17:25:29 +05:30
def protected_for?(ref)
2019-01-03 12:48:30 +05:30
raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
resolved_ref = repository.expand_ref(ref) || ref
return false unless Gitlab::Git.tag_ref?(resolved_ref) || Gitlab::Git.branch_ref?(resolved_ref)
ref_name = if resolved_ref == ref
Gitlab::Git.ref_name(resolved_ref)
else
ref
end
if Gitlab::Git.branch_ref?(resolved_ref)
ProtectedBranch.protected?(self, ref_name)
elsif Gitlab::Git.tag_ref?(resolved_ref)
ProtectedTag.protected?(self, ref_name)
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
end
2018-05-09 12:01:36 +05:30
def deployment_variables(environment: nil)
2018-12-13 13:39:08 +05:30
deployment_platform(environment: environment)&.predefined_variables(project: self) || []
2018-03-17 18:26:18 +05:30
end
2016-08-24 12:49:21 +05:30
2018-03-17 18:26:18 +05:30
def auto_devops_variables
return [] unless auto_devops_enabled?
2018-05-09 12:01:36 +05:30
(auto_devops || build_auto_devops)&.predefined_variables
2016-08-24 12:49:21 +05:30
end
def append_or_update_attribute(name, value)
2018-03-17 18:26:18 +05:30
old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
2016-08-24 12:49:21 +05:30
if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
update_attribute(name, old_values + value)
else
update_attribute(name, value)
end
2017-08-17 22:00:37 +05:30
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
2016-08-24 12:49:21 +05:30
end
2019-02-15 15:39:39 +05:30
# Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
#
# @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
def set_repository_read_only!
with_lock do
break false if git_transfer_in_progress?
update_column(:repository_read_only, true)
end
end
# Set repository as writable again
def set_repository_writable!
with_lock do
2019-03-13 22:55:13 +05:30
update_column(:repository_read_only, false)
2019-02-15 15:39:39 +05:30
end
end
2016-09-29 09:46:39 +05:30
def pushes_since_gc
2017-09-10 17:25:29 +05:30
Gitlab::Redis::SharedState.with { |redis| redis.get(pushes_since_gc_redis_shared_state_key).to_i }
2016-09-29 09:46:39 +05:30
end
def increment_pushes_since_gc
2017-09-10 17:25:29 +05:30
Gitlab::Redis::SharedState.with { |redis| redis.incr(pushes_since_gc_redis_shared_state_key) }
2016-09-29 09:46:39 +05:30
end
def reset_pushes_since_gc
2017-09-10 17:25:29 +05:30
Gitlab::Redis::SharedState.with { |redis| redis.del(pushes_since_gc_redis_shared_state_key) }
2016-09-29 09:46:39 +05:30
end
2017-08-17 22:00:37 +05:30
def route_map_for(commit_sha)
@route_maps_by_commit ||= Hash.new do |h, sha|
h[sha] = begin
data = repository.route_map_for(sha)
next unless data
2016-09-29 09:46:39 +05:30
2017-08-17 22:00:37 +05:30
Gitlab::RouteMap.new(data)
rescue Gitlab::RouteMap::FormatError
nil
2016-09-29 09:46:39 +05:30
end
2017-08-17 22:00:37 +05:30
end
@route_maps_by_commit[commit_sha]
end
2016-09-29 09:46:39 +05:30
2017-08-17 22:00:37 +05:30
def public_path_for_source_path(path, commit_sha)
map = route_map_for(commit_sha)
return unless map
map.public_path_for_source_path(path)
end
def parent_changed?
namespace_id_changed?
end
def default_merge_request_target
if forked_from_project&.merge_requests_enabled?
forked_from_project
else
self
2016-09-29 09:46:39 +05:30
end
end
2018-03-27 19:54:05 +05:30
# Overridden on EE module
def multiple_issue_boards_available?
false
2018-03-17 18:26:18 +05:30
end
def full_path_was
File.join(namespace.full_path, previous_changes['path'].first)
end
2017-08-17 22:00:37 +05:30
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
2017-09-10 17:25:29 +05:30
# @deprecated cannot remove yet because it has an index with its name in elasticsearch
2017-08-17 22:00:37 +05:30
alias_method :path_with_namespace, :full_path
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2017-09-10 17:25:29 +05:30
def forks_count
Projects::ForksCountService.new(self).count
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-09-10 17:25:29 +05:30
def legacy_storage?
2018-03-17 18:26:18 +05:30
[nil, 0].include?(self.storage_version)
end
# Check if Hashed Storage is enabled for the project with at least informed feature rolled out
#
# @param [Symbol] feature that needs to be rolled out for the project (:repository, :attachments)
def hashed_storage?(feature)
raise ArgumentError, "Invalid feature" unless HASHED_STORAGE_FEATURES.include?(feature)
self.storage_version && self.storage_version >= HASHED_STORAGE_FEATURES[feature]
end
def renamed?
persisted? && path_changed?
end
2019-07-07 11:18:12 +05:30
def human_merge_method
if merge_method == :ff
'Fast-forward'
else
merge_method.to_s.humanize
end
end
2018-03-17 18:26:18 +05:30
def merge_method
if self.merge_requests_ff_only_enabled
:ff
elsif self.merge_requests_rebase_enabled
:rebase_merge
else
:merge
end
end
def merge_method=(method)
case method.to_s
when "ff"
self.merge_requests_ff_only_enabled = true
self.merge_requests_rebase_enabled = true
when "rebase_merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = true
when "merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = false
end
end
def ff_merge_must_be_possible?
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
def migrate_to_hashed_storage!
2019-02-15 15:39:39 +05:30
return unless storage_upgradable?
2018-03-17 18:26:18 +05:30
2019-02-15 15:39:39 +05:30
if git_transfer_in_progress?
2019-07-07 11:18:12 +05:30
HashedStorage::ProjectMigrateWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
else
HashedStorage::ProjectMigrateWorker.perform_async(id)
end
end
def rollback_to_legacy_storage!
return if legacy_storage?
if git_transfer_in_progress?
HashedStorage::ProjectRollbackWorker.perform_in(Gitlab::ReferenceCounter::REFERENCE_EXPIRE_TIME, id)
2018-03-17 18:26:18 +05:30
else
2019-07-07 11:18:12 +05:30
HashedStorage::ProjectRollbackWorker.perform_async(id)
2018-03-17 18:26:18 +05:30
end
end
2019-02-15 15:39:39 +05:30
def git_transfer_in_progress?
repo_reference_count > 0 || wiki_reference_count > 0
end
2018-03-17 18:26:18 +05:30
def storage_version=(value)
super
@storage = nil if storage_version_changed?
end
2019-07-07 11:18:12 +05:30
def reference_counter(type: Gitlab::GlRepository::PROJECT)
Gitlab::ReferenceCounter.new(type.identifier_for_subject(self))
2018-03-17 18:26:18 +05:30
end
2018-03-27 19:54:05 +05:30
def badges
return project_badges unless group
2018-12-05 23:21:45 +05:30
Badge.from_union([
project_badges,
GroupBadge.where(group: group.self_and_ancestors)
])
2018-03-27 19:54:05 +05:30
end
def merge_requests_allowing_push_to_user(user)
return MergeRequest.none unless user
developer_access_exists = user.project_authorizations
.where('access_level >= ? ', Gitlab::Access::DEVELOPER)
.where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1)
.select(1)
2019-02-15 15:39:39 +05:30
merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
2018-03-27 19:54:05 +05:30
end
2019-02-15 15:39:39 +05:30
def any_branch_allows_collaboration?(user)
fetch_branch_allows_collaboration(user)
end
2018-03-27 19:54:05 +05:30
2019-02-15 15:39:39 +05:30
def branch_allows_collaboration?(user, branch_name)
fetch_branch_allows_collaboration(user, branch_name)
2018-03-27 19:54:05 +05:30
end
2019-07-07 11:18:12 +05:30
def external_authorization_classification_label
super || ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
end
2018-10-15 14:42:47 +05:30
def licensed_features
[]
end
def toggle_ci_cd_settings!(settings_attribute)
ci_cd_settings.toggle!(settings_attribute)
end
def gitlab_deploy_token
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
end
2018-11-08 19:23:39 +05:30
def any_lfs_file_locks?
lfs_file_locks.any?
end
request_cache(:any_lfs_file_locks?) { self.id }
def auto_cancel_pending_pipelines?
auto_cancel_pending_pipelines == 'enabled'
end
2018-03-17 18:26:18 +05:30
def storage
@storage ||=
if hashed_storage?(:repository)
Storage::HashedProject.new(self)
else
Storage::LegacyProject.new(self)
end
end
2018-12-13 13:39:08 +05:30
def storage_upgradable?
storage_version != LATEST_STORAGE_VERSION
end
def snippets_visible?(user = nil)
Ability.allowed?(user, :read_project_snippet, self)
end
2019-02-15 15:39:39 +05:30
def max_attachment_size
Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
def object_pool_params
return {} unless !forked? && git_objects_poolable?
{
repository_storage: repository_storage,
pool_repository: pool_repository || create_new_pool_repository
}
end
# Git objects are only poolable when the project is or has:
# - Hashed storage -> The object pool will have a remote to its members, using relative paths.
# If the repository path changes we would have to update the remote.
# - Public -> User will be able to fetch Git objects that might not exist
# in their own repository.
# - Repository -> Else the disk path will be empty, and there's nothing to pool
def git_objects_poolable?
hashed_storage?(:repository) &&
public? &&
repository_exists? &&
Gitlab::CurrentSettings.hashed_storage_enabled &&
Feature.enabled?(:object_pools, self)
end
def leave_pool_repository
2019-07-07 11:18:12 +05:30
pool_repository&.mark_obsolete_if_last(repository) && update_column(:pool_repository_id, nil)
2019-03-02 22:35:43 +05:30
end
def link_pool_repository
pool_repository&.link_repository(repository)
2019-02-15 15:39:39 +05:30
end
2019-07-07 11:18:12 +05:30
def has_pool_repository?
pool_repository.present?
end
2018-12-13 13:39:08 +05:30
private
2019-02-15 15:39:39 +05:30
def merge_requests_allowing_collaboration(source_branch = nil)
relation = source_of_merge_requests.opened.where(allow_collaboration: true)
relation = relation.where(source_branch: source_branch) if source_branch
relation
end
def create_new_pool_repository
pool = begin
create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
rescue ActiveRecord::RecordNotUnique
pool_repository(true)
end
pool.schedule unless pool.scheduled?
pool
end
def join_pool_repository
return unless pool_repository
ObjectPool::JoinWorker.perform_async(pool_repository.id, self.id)
end
2018-03-17 18:26:18 +05:30
def use_hashed_storage
if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled
self.storage_version = LATEST_STORAGE_VERSION
end
end
def repo_reference_count
reference_counter.value
end
def wiki_reference_count
2019-07-07 11:18:12 +05:30
reference_counter(type: Gitlab::GlRepository::WIKI).value
2018-03-17 18:26:18 +05:30
end
def check_repository_absence!
return if skip_disk_validation
2018-10-15 14:42:47 +05:30
if repository_storage.blank? || repository_with_same_path_already_exists?
2018-03-17 18:26:18 +05:30
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
2018-10-15 14:42:47 +05:30
gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
2018-03-17 18:26:18 +05:30
end
2018-12-13 13:39:08 +05:30
def set_timestamps_for_create
update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
def cross_namespace_reference?(from)
case from
when Project
namespace != from.namespace
when Namespace
namespace != from
end
end
# Check if a reference is being done cross-project
def cross_project_reference?(from)
return true if from.is_a?(Namespace)
from && self != from
end
2017-09-10 17:25:29 +05:30
def pushes_since_gc_redis_shared_state_key
2016-09-29 09:46:39 +05:30
"projects/#{id}/pushes_since_gc"
end
2017-08-17 22:00:37 +05:30
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
2016-08-24 12:49:21 +05:30
end
2017-08-17 22:00:37 +05:30
def check_pending_delete
return if valid_attribute?(:name) && valid_attribute?(:path)
return unless pending_delete_twin
%i[route route.path name path].each do |error|
errors.delete(error)
end
2016-08-24 12:49:21 +05:30
2017-08-17 22:00:37 +05:30
errors.add(:base, "The project is still being deleted. Please try again later.")
2016-08-24 12:49:21 +05:30
end
2017-08-17 22:00:37 +05:30
def pending_delete_twin
return false unless path
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
Project.pending_delete.find_by_full_path(full_path)
2017-08-17 22:00:37 +05:30
end
2016-08-24 12:49:21 +05:30
2017-08-17 22:00:37 +05:30
##
# This method is here because of support for legacy container repository
# which has exactly the same path like project does, but which might not be
# persisted in `container_repositories` table.
#
def has_root_container_repository_tags?
return false unless Gitlab.config.registry.enabled
ContainerRepository.build_root_repository(self).has_tags?
2016-08-24 12:49:21 +05:30
end
2016-11-03 12:29:30 +05:30
2017-08-17 22:00:37 +05:30
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
2016-11-03 12:29:30 +05:30
end
2018-03-27 19:54:05 +05:30
2019-02-15 15:39:39 +05:30
def fetch_branch_allows_collaboration(user, branch_name = nil)
return false unless user
2018-05-09 12:01:36 +05:30
2019-02-15 15:39:39 +05:30
Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
next false if empty_repo?
2018-11-08 19:23:39 +05:30
2018-11-18 11:00:15 +05:30
# Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322
Gitlab::GitalyClient.allow_n_plus_1_calls do
2019-02-15 15:39:39 +05:30
merge_requests_allowing_collaboration(branch_name).any? do |merge_request|
merge_request.can_be_merged_by?(user)
2018-11-18 11:00:15 +05:30
end
2018-11-08 19:23:39 +05:30
end
2018-03-27 19:54:05 +05:30
end
end
2018-12-05 23:21:45 +05:30
def services_templates
@services_templates ||= Service.where(template: true)
end
2014-09-02 18:07:02 +05:30
end