#!/usr/bin/env ruby # frozen_string_literal: true # rubocop:disable Style/TopLevelMethodDefinition require "find" require "json" require "open-uri" require "open3" require "optparse" require "pathname" require "tmpdir" # Exit cleanup TMP_DIR = Pathname.new(Dir.mktmpdir).freeze at_exit { TMP_DIR.rmtree } # Constants ONLINE_ISSUE = "https://github.com/Homebrew/homebrew-cask/issues/88469" CASK_REPOS = %w[homebrew-cask homebrew-cask-versions].freeze CASK_JSON_URL = "https://formulae.brew.sh/api/analytics/cask-install/365d.json" # Download the file and save it to the specified directory File.open("#{TMP_DIR}/cask.json", "wb") do |output_file| URI.parse(CASK_JSON_URL).open do |input_file| output_file.write(input_file.read) end end CASK_JSON = File.read("#{TMP_DIR}/cask.json").freeze CASK_DATA = JSON.parse(CASK_JSON).freeze # Helpers def cask_name(cask_path) cask_path.basename.sub(/\.rb$/, "") end def cask_url(tap_dir, cask_path) tap_base = tap_dir.dirname.basename.to_path cask_base = cask_path.relative_path_from(tap_dir).to_path "https://github.com/Homebrew/#{tap_base}/blob/master/Casks/#{cask_base}" end def find_count(cask_name, data) data["items"].find { |item| item["cask"] == cask_name.to_s }&.dig("count") || "0" end # Options ARGV.push("--help") unless ARGV.include?("run") OptionParser.new do |parser| parser.banner = <<~BANNER Generates lists of casks missing `zap` in official repos, and copies it to replace the information on #{ONLINE_ISSUE} Usage: #{File.basename($PROGRAM_NAME)} run The argument 'run' is necessary to prevent running the script by mistake. BANNER end.parse! # Grab all taps and casks CASK_DIRS = CASK_REPOS.each_with_object([]) do |repo, tap_dirs| clone_dir = TMP_DIR.join(repo) casks_dir = clone_dir.join("Casks") tap_dirs.push(casks_dir) system("git", "clone", "--depth", "1", "https://github.com/Homebrew/#{repo}.git", clone_dir.to_path) end.freeze # Collect all casks from each tap directory into a hash ALL_CASKS = CASK_DIRS.each_with_object({}) do |tap_dir, casks| casks[tap_dir] = [] # Recursively find all Ruby files in the tap directory Find.find(tap_dir) do |path| # Skip if not a file or not a Ruby file next unless File.file?(path) next if File.extname(path) != ".rb" # Add the path to the casks array for the current tap casks[tap_dir].push(Pathname.new(path)) end end.freeze # Filter casks that are missing a 'zap' stanza and are not discontinued CASKS_NO_ZAP = ALL_CASKS.each_with_object({}) do |(tap_dir, casks), without_zap| without_zap[tap_dir] = casks.reject do |file| # Read file content and check if it contains 'zap' or 'discontinued' file_content = file.read file_content.match?(/\s+(# No )?zap /) || file_content.match?(/\s+discontinued /) end # Remove the tap directory from the result if it has no casks missing 'zap' without_zap.delete(tap_dir) if without_zap[tap_dir].empty? end.freeze CASK_LISTS = CASKS_NO_ZAP.each_with_object([]) do |(tap_dir, casks), message| message.push("
#{tap_dir.dirname.basename.to_path}") message.push("") # Empty line so the markdown still works inside the HTML message.push("| Cask | Downloads |", "| :--- | ---: |") # Table header # Sort casks by count sorted_casks = casks.sort_by { |cask_file| -find_count(cask_name(cask_file), CASK_DATA).delete(",").to_i } sorted_casks.each do |cask_file| cask_name = cask_name(cask_file) count = find_count(cask_name, CASK_DATA) message.push("| [#{cask_name}](#{cask_url(tap_dir, cask_file)}) | #{count} |") end message.push("
") end.freeze Open3.capture2("/usr/bin/pbcopy", stdin_data: "#{CASK_LISTS.join("\n")}\n") puts("Copied lists to clipboard. Replace the information in the issue.") system("open", ONLINE_ISSUE) # rubocop:enable Style/TopLevelMethodDefinition