common/lib/dependabot/pull_request_creator/commit_signer.rb
# typed: strict
# frozen_string_literal: true

require "time"
require "tmpdir"
require "sorbet-runtime"
require "dependabot/pull_request_creator"

module Dependabot
  class PullRequestCreator
    class CommitSigner
      extend T::Sig

      sig { returns(T::Hash[Symbol, String]) }
      attr_reader :author_details

      sig { returns(String) }
      attr_reader :commit_message

      sig { returns(String) }
      attr_reader :tree_sha

      sig { returns(String) }
      attr_reader :parent_sha

      sig { returns(String) }
      attr_reader :signature_key

      sig do
        params(
          author_details: T::Hash[Symbol, String],
          commit_message: String,
          tree_sha: String,
          parent_sha: String,
          signature_key: String
        ).void
      end
      def initialize(
        author_details:,
        commit_message:,
        tree_sha:,
        parent_sha:,
        signature_key:
      )
        @author_details = author_details
        @commit_message = commit_message
        @tree_sha = tree_sha
        @parent_sha = parent_sha
        @signature_key = signature_key
      end

      sig { returns(String) }
      def signature
        begin
          require "gpgme"
        rescue LoadError
          raise LoadError,
                "Please add `gpgme` to your Gemfile or gemspec " \
                "enable commit signatures"
        end

        email = author_details[:email]

        dir = Dir.mktmpdir

        GPGME::Engine.home_dir = dir
        GPGME::Key.import(signature_key)

        crypto = GPGME::Crypto.new(armor: true)
        opts = { mode: GPGME::SIG_MODE_DETACH, signer: email }
        crypto.sign(commit_object, opts).to_s
      rescue Errno::ENOTEMPTY
        FileUtils.remove_entry(T.must(dir), true)
        # This appears to be a Ruby bug which occurs very rarely
        raise if @retrying

        @retrying = T.let(true, T.nilable(T::Boolean))
        retry
      ensure
        FileUtils.remove_entry(T.must(dir), true)
      end

      private

      sig { returns(String) }
      def commit_object
        time_str = Time.parse(T.must(author_details[:date])).strftime("%s %z")
        name = author_details[:name]
        email = author_details[:email]

        [
          "tree #{tree_sha}",
          "parent #{parent_sha}",
          "author #{name} <#{email}> #{time_str}",
          "committer #{name} <#{email}> #{time_str}",
          "",
          commit_message
        ].join("\n")
      end
    end
  end
end
