lib/airbrake-ruby/filters/git_last_checkout_filter.rb



require 'date'

module Airbrake
  module Filters
    # Attaches git checkout info to `context`. The info includes:
    #   * username
    #   * email
    #   * revision
    #   * time
    #
    # This information is used to track deploys automatically.
    #
    # @api private
    # @since v2.12.0
    class GitLastCheckoutFilter
      # @return [Integer]
      attr_reader :weight

      # @return [Integer] least possible amount of columns in git's `logs/HEAD`
      #   file (checkout information is omitted)
      MIN_HEAD_COLS = 6

      include Loggable

      # @param [String] root_directory
      def initialize(root_directory)
        @git_path = File.join(root_directory, '.git')
        @weight = 116
        @last_checkout = nil
        @deploy_username = ENV.fetch('AIRBRAKE_DEPLOY_USERNAME', nil)
      end

      # @macro call_filter
      def call(notice)
        return if notice[:context].key?(:lastCheckout)

        if @last_checkout
          notice[:context][:lastCheckout] = @last_checkout
          return
        end

        return unless File.exist?(@git_path)
        return unless (checkout = last_checkout)

        notice[:context][:lastCheckout] = checkout
      end

      private

      def last_checkout
        return unless (line = last_checkout_line)

        parts = line.chomp.split("\t").first.split
        if parts.size < MIN_HEAD_COLS
          logger.error(
            "#{LOG_LABEL} Airbrake::#{self.class.name}: can't parse line: #{line}",
          )
          return
        end

        author = parts[2..-4]
        @last_checkout = {
          username: @deploy_username || author[0..1].join(' '),
          email: parts[-3][1..-2],
          revision: parts[1],
          time: timestamp(parts[-2].to_i),
        }
      end

      def last_checkout_line
        head_path = File.join(@git_path, 'logs', 'HEAD')
        return unless File.exist?(head_path)

        last_line = nil
        File.foreach(head_path) do |line|
          last_line = line if checkout_line?(line)
        end
        last_line
      end

      def checkout_line?(line)
        line.include?("\tclone:") ||
          line.include?("\tpull:") ||
          line.include?("\tcheckout:")
      end

      def timestamp(utime)
        Time.at(utime).to_datetime.rfc3339
      end
    end
  end
end