gitlabhq/gitlabhq

View on GitHub
app/services/groups/transfer_service.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true

module Groups
  class TransferService < Groups::BaseService
    ERROR_MESSAGES = {
      database_not_supported: s_('TransferGroup|Database is not supported.'),
      namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'),
      group_is_already_root: s_('TransferGroup|Group is already a root group.'),
      same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
      invalid_policies: s_("TransferGroup|You don't have enough permissions.")
    }.freeze

    TransferError = Class.new(StandardError)

    attr_reader :error

    def initialize(group, user, params = {})
      super
      @error = nil
    end

    def execute(new_parent_group)
      @new_parent_group = new_parent_group
      ensure_allowed_transfer
      proceed_to_transfer

    rescue TransferError, ActiveRecord::RecordInvalid, Gitlab::UpdatePathError => e
      @group.errors.clear
      @error = s_("TransferGroup|Transfer failed: %{error_message}") % { error_message: e.message }
      false
    end

    private

    def proceed_to_transfer
      Group.transaction do
        update_group_attributes
        ensure_ownership
      end

      true
    end

    def ensure_allowed_transfer
      raise_transfer_error(:group_is_already_root) if group_is_already_root?
      raise_transfer_error(:database_not_supported) unless Group.supports_nested_objects?
      raise_transfer_error(:same_parent_as_current) if same_parent?
      raise_transfer_error(:invalid_policies) unless valid_policies?
      raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
    end

    def group_is_already_root?
      !@new_parent_group && !@group.has_parent?
    end

    def same_parent?
      @new_parent_group && @new_parent_group.id == @group.parent_id
    end

    def valid_policies?
      return false unless can?(current_user, :admin_group, @group)

      if @new_parent_group
        can?(current_user, :create_subgroup, @new_parent_group)
      else
        can?(current_user, :create_group)
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def namespace_with_same_path?
      Namespace.exists?(path: @group.path, parent: @new_parent_group)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def update_group_attributes
      if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
        update_children_and_projects_visibility
        @group.visibility_level = @new_parent_group.visibility_level
      end

      @group.parent = @new_parent_group
      @group.save!
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def update_children_and_projects_visibility
      descendants = @group.descendants.where("visibility_level > ?", @new_parent_group.visibility_level)

      Group
        .where(id: descendants.select(:id))
        .update_all(visibility_level: @new_parent_group.visibility_level)

      @group
        .all_projects
        .where("visibility_level > ?", @new_parent_group.visibility_level)
        .update_all(visibility_level: @new_parent_group.visibility_level)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def ensure_ownership
      return if @new_parent_group
      return unless @group.owners.empty?

      @group.add_owner(current_user)
    end

    def raise_transfer_error(message)
      raise TransferError, ERROR_MESSAGES[message]
    end
  end
end