namespace.rb 7.9 KB
Newer Older
1
class Namespace < ActiveRecord::Base
2
  acts_as_paranoid without_default_scope: true
3

4
  include CacheMarkdownField
5
  include Sortable
6
  include Gitlab::ShellAdapter
7
  include Gitlab::CurrentSettings
8
  include Gitlab::VisibilityLevel
9
  include Routable
10
  include AfterCommitQueue
11
  include Storage::LegacyNamespace
12
  include Gitlab::SQL::Pattern
13

14 15 16 17 18
  # Prevent users from creating unreasonably deep level of nesting.
  # The number 20 was taken based on maximum nesting level of
  # Android repo (15) + some extra backup.
  NUMBER_OF_ANCESTORS_ALLOWED = 20

19 20
  cache_markdown_field :description, pipeline: :description

21
  has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
M
Markus Koller 已提交
22
  has_many :project_statistics
23 24
  belongs_to :owner, class_name: "User"

25 26
  belongs_to :parent, class_name: "Namespace"
  has_many :children, class_name: "Namespace", foreign_key: :parent_id
27
  has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
28

29
  validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
30
  validates :name,
31
    presence: true,
32
    uniqueness: { scope: :parent_id },
33 34
    length: { maximum: 255 },
    namespace_name: true
35

36
  validates :description, length: { maximum: 255 }
37
  validates :path,
R
Robert Speicher 已提交
38
    presence: true,
39
    length: { maximum: 255 },
40
    namespace_path: true
41

42
  validate :nesting_level_allowed
43
  validate :allowed_path_by_redirects
44

45 46
  delegate :name, to: :owner, allow_nil: true, prefix: true

47
  after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
48

49 50
  before_create :sync_share_with_group_lock_with_parent
  before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
M
Michael Kozono 已提交
51
  after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
52

53 54 55
  # Legacy Storage specific hooks

  after_update :move_dir, if: :path_changed?
56
  before_destroy(prepend: true) { prepare_for_destroy }
57
  after_destroy :rm_dir
58

59
  scope :for_user, -> { where('type IS NULL') }
60

M
Markus Koller 已提交
61 62 63 64 65 66 67 68
  scope :with_statistics, -> do
    joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
      .group('namespaces.id')
      .select(
        'namespaces.*',
        'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
        'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
        'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
69
        'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size'
M
Markus Koller 已提交
70 71 72
      )
  end

73 74
  class << self
    def by_path(path)
G
Gabriel Mazetto 已提交
75
      find_by('lower(path) = :value', value: path.downcase)
76 77 78 79 80 81 82
    end

    # Case insensetive search for namespace by path or name
    def find_by_path_or_name(path)
      find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
    end

83 84 85 86 87 88 89
    # Searches for namespaces matching the given query.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String
    #
    # Returns an ActiveRecord::Relation
90
    def search(query)
91
      fuzzy_search(query, [:name, :path])
92 93 94
    end

    def clean_path(path)
95
      path = path.dup
D
Douwe Maan 已提交
96
      # Get the email username by removing everything after an `@` sign.
97
      path.gsub!(/@.*\z/,                "")
D
Douwe Maan 已提交
98
      # Remove everything that's not in the list of allowed characters.
99 100 101 102 103
      path.gsub!(/[^a-zA-Z0-9_\-\.]/,    "")
      # Remove trailing violations ('.atom', '.git', or '.')
      path.gsub!(/(\.atom|\.git|\.)*\z/, "")
      # Remove leading violations ('-')
      path.gsub!(/\A\-+/,                "")
104

105
      # Users with the great usernames of "." or ".." would end up with a blank username.
106
      # Work around that by setting their username to "blank", followed by a counter.
107 108
      path = "blank" if path.blank?

109
      uniquify = Uniquify.new
110
      uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
111
    end
112 113
  end

114 115 116 117
  def visibility_level_field
    :visibility_level
  end

118
  def to_param
119
    full_path
120
  end
121 122 123 124

  def human_name
    owner_name
  end
125

126
  def any_project_has_container_registry_tags?
127
    all_projects.any?(&:has_container_registry_tags?)
128 129
  end

130 131 132 133 134 135
  def send_update_instructions
    projects.each do |project|
      project.send_move_instructions("#{full_path_was}/#{project.path}")
    end
  end

136 137 138
  def kind
    type == 'Group' ? 'group' : 'user'
  end
139 140

  def find_fork_of(project)
141 142
    return nil unless project.fork_network

143 144 145 146 147 148 149 150 151 152 153
    if RequestStore.active?
      forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do
        Hash.new do |found_forks, project|
          found_forks[project] = project.fork_network.find_forks_in(projects).first
        end
      end

      forks_in_namespace[project]
    else
      project.fork_network.find_forks_in(projects).first
    end
154
  end
155

156 157 158 159 160
  def lfs_enabled?
    # User namespace will always default to the global setting
    Gitlab.config.lfs.enabled
  end

161 162 163 164
  def shared_runners_enabled?
    projects.with_shared_runners.any?
  end

165
  # Returns all the ancestors of the current namespaces.
166
  def ancestors
167
    return self.class.none unless parent_id
168

169 170 171
    Gitlab::GroupHierarchy
      .new(self.class.where(id: parent_id))
      .base_and_ancestors
172 173
  end

174 175 176 177 178 179 180
  # returns all ancestors upto but excluding the the given namespace
  # when no namespace is given, all ancestors upto the top are returned
  def ancestors_upto(top = nil)
    Gitlab::GroupHierarchy.new(self.class.where(id: id))
      .ancestors(upto: top)
  end

181 182 183 184 185 186 187 188
  def self_and_ancestors
    return self.class.where(id: id) unless parent_id

    Gitlab::GroupHierarchy
      .new(self.class.where(id: id))
      .base_and_ancestors
  end

189
  # Returns all the descendants of the current namespace.
190
  def descendants
191 192 193
    Gitlab::GroupHierarchy
      .new(self.class.where(parent_id: id))
      .base_and_descendants
194 195
  end

196 197 198 199 200 201
  def self_and_descendants
    Gitlab::GroupHierarchy
      .new(self.class.where(id: id))
      .base_and_descendants
  end

202 203 204 205
  def user_ids_for_project_authorizations
    [owner_id]
  end

206 207 208 209
  def parent_changed?
    parent_id_changed?
  end

210 211 212 213 214 215
  # Includes projects from this namespace and projects from all subgroups
  # that belongs to this namespace
  def all_projects
    Project.inside_path(full_path)
  end

216 217 218 219
  def has_parent?
    parent.present?
  end

220 221 222 223
  def subgroup?
    has_parent?
  end

224 225 226 227 228 229
  def soft_delete_without_removing_associations
    # We can't use paranoia's `#destroy` since this will hard-delete projects.
    # Project uses `pending_delete` instead of the acts_as_paranoia gem.
    self.deleted_at = Time.now
  end

230 231
  private

232
  def refresh_access_of_projects_invited_groups
233 234 235 236
    Group
      .joins(project_group_links: :project)
      .where(projects: { namespace_id: id })
      .find_each(&:refresh_members_authorized_projects)
237
  end
238

239 240 241 242 243
  def nesting_level_allowed
    if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
      errors.add(:parent_id, "has too deep level of nesting")
    end
  end
244 245

  def sync_share_with_group_lock_with_parent
M
Michael Kozono 已提交
246
    if parent&.share_with_group_lock?
247 248 249 250 251
      self.share_with_group_lock = true
    end
  end

  def force_share_with_group_lock_on_descendants
252 253 254 255 256 257 258 259
    return unless Group.supports_nested_groups?

    # We can't use `descendants.update_all` since Rails will throw away the WITH
    # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
    # different table aliases, hence we're just using WHERE IN. Since we have a
    # maximum of 20 nested groups this should be fine.
    Namespace.where(id: descendants.select(:id))
      .update_all(share_with_group_lock: true)
260
  end
261 262 263 264 265 266 267 268 269 270

  def allowed_path_by_redirects
    return if path.nil?

    errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
  end

  def namespace_previously_created_with_same_path?
    RedirectRoute.permanent.exists?(path: path)
  end
271

272 273
  def write_projects_repository_config
    all_projects.find_each do |project|
274
      project.expires_full_path_cache # we need to clear cache to validate renames correctly
275
      project.write_repository_config
276 277
    end
  end
278
end