Skip to content
20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb 3.78 KiB
Newer Older
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.

# This migration depends on code external to it. For example, it relies on
# updating a namespace to also rename directories (uploads, GitLab pages, etc).
# The alternative is to copy hundreds of lines of code into this migration,
# adjust them where needed, etc; something which doesn't work well at all.
class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration[4.2]
  include Gitlab::Database::MigrationHelpers

  # Set this constant to true if this migration requires downtime.
  DOWNTIME = false

  def run_migration?
    Gitlab::Database.mysql?
  end

  def up
    return unless run_migration?

    # For all sub-groups we need to give the right people access. We do this as
    # follows:
    #
    # 1. Get all the ancestors for the current namespace
    # 2. Get all the members of these namespaces, along with their higher access
    #    level
    # 3. Give these members access to the current namespace
    Namespace.unscoped.where('parent_id IS NOT NULL').find_each do |namespace|
      rows = []
      existing = namespace.members.pluck(:user_id)

      all_members_for(namespace).each do |member|
        next if existing.include?(member[:user_id])

        rows << {
          access_level: member[:access_level],
          source_id: namespace.id,
          source_type: 'Namespace',
          user_id: member[:user_id],
          notification_level: 3, # global
          type: 'GroupMember',
          created_at: Time.current,
          updated_at: Time.current
        }
      end

      bulk_insert_members(rows)

      # This method relies on the parent to determine the proper path.
      # Because we reset "parent_id" this method will not return the right path
      # when moving namespaces.
      full_path_was = namespace.send(:full_path_was)

      namespace.define_singleton_method(:full_path_was) { full_path_was }

      namespace.update!(parent_id: nil, path: new_path_for(namespace))
    end
  end

  def down
    # There is no way to go back from regular groups to nested groups.
  end

  # Generates a new (unique) path for a namespace.
  def new_path_for(namespace)
    counter = 1
    base = namespace.full_path.tr('/', '-')
    new_path = base

    while Namespace.unscoped.where(path: new_path).exists?
      new_path = base + "-#{counter}"
      counter += 1
    end

    new_path
  end

  # Returns an Array containing all the ancestors of the current namespace.
  #
  # This method is not particularly efficient, but it's probably still faster
  # than using the "routes" table. Most importantly of all, it _only_ depends
  # on the namespaces table and the "parent_id" column.
  def ancestors_for(namespace)
    ancestors = []
    current = namespace

    while current&.parent_id
      # We're using find_by(id: ...) here to deal with cases where the
      # parent_id may point to a missing row.
      current = Namespace.unscoped.select([:id, :parent_id])
        .find_by(id: current.parent_id)

      ancestors << current.id if current
    end

    ancestors
  end

  # Returns a relation containing all the members that have access to any of
  # the current namespace's parent namespaces.
  def all_members_for(namespace)
    Member
      .unscoped
      .select(['user_id', 'MAX(access_level) AS access_level'])
      .where(source_type: 'Namespace', source_id: ancestors_for(namespace))
      .group(:user_id)
  end

  def bulk_insert_members(rows)
    return if rows.empty?

    keys = rows.first.keys

    tuples = rows.map do |row|
      row.map { |(_, value)| connection.quote(value) }
    end

    execute <<-EOF.strip_heredoc
    INSERT INTO members (#{keys.join(', ')})
    VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
    EOF
  end
end