common/lib/dependabot/pull_request_creator/branch_namer/dependency_group_strategy.rb
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/pull_request_creator/branch_namer/base"

module Dependabot
  class PullRequestCreator
    class BranchNamer
      class DependencyGroupStrategy < Base
        extend T::Sig

        sig do
          params(
            dependencies: T::Array[Dependabot::Dependency],
            files: T::Array[Dependabot::DependencyFile],
            target_branch: T.nilable(String),
            dependency_group: Dependabot::DependencyGroup,
            includes_security_fixes: T::Boolean,
            separator: String,
            prefix: String,
            max_length: T.nilable(Integer)
          )
            .void
        end
        def initialize(
          dependencies:,
          files:,
          target_branch:,
          dependency_group:,
          includes_security_fixes:,
          separator: "/",
          prefix: "dependabot",
          max_length: nil
        )
          super(
            dependencies: dependencies,
            files: files,
            target_branch: target_branch,
            separator: separator,
            prefix: prefix,
            max_length: max_length,
          )

          @dependency_group = dependency_group
          @includes_security_fixes = includes_security_fixes
        end

        sig { override.returns(String) }
        def new_branch_name
          sanitize_branch_name(File.join(prefixes, group_name_with_dependency_digest))
        end

        private

        sig { returns(Dependabot::DependencyGroup) }
        attr_reader :dependency_group

        sig { returns(T::Array[String]) }
        def prefixes
          [
            prefix,
            package_manager,
            directory,
            target_branch
          ].compact
        end

        # Group pull requests will generally include too many dependencies to include
        # in the branch name, but we rely on branch names being deterministic for a
        # given set of dependency changes.
        #
        # Let's append a short hash digest of the dependency changes so that we can
        # meet this guarantee.
        sig { returns(String) }
        def group_name_with_dependency_digest
          if @includes_security_fixes
            "group-security-#{package_manager}-#{dependency_digest}"
          else
            "#{dependency_group.name}-#{dependency_digest}"
          end
        end

        sig { returns(T.nilable(String)) }
        def dependency_digest
          @dependency_digest ||= T.let(
            Digest::MD5.hexdigest(
              dependencies.map do |dependency|
                "#{dependency.name}-#{dependency.removed? ? 'removed' : dependency.version}"
              end.sort.join(",")
            ).slice(0, 10),
            T.nilable(String)
          )
        end

        sig { returns(String) }
        def package_manager
          T.must(dependencies.first).package_manager
        end

        sig { returns(T.nilable(String)) }
        def directory
          return if files.empty?

          T.must(files.first).directory.tr(" ", "-")
        end
      end
    end
  end
end
