Merge pull request #17606 from Homebrew/sorbet-strict-devcmd

This commit is contained in:
Mike McQuaid 2024-07-04 08:25:40 +01:00 committed by GitHub
commit 3773940382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 246 additions and 84 deletions

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -329,6 +329,7 @@ module Homebrew
private private
sig { params(results: T::Hash[[Symbol, Pathname], T::Array[T::Hash[Symbol, T.untyped]]]).void }
def print_problems(results) def print_problems(results)
results.each do |(name, path), problems| results.each do |(name, path), problems|
problem_lines = format_problem_lines(problems) problem_lines = format_problem_lines(problems)
@ -343,6 +344,7 @@ module Homebrew
end end
end end
sig { params(problems: T::Array[T::Hash[Symbol, T.untyped]]).returns(T::Array[String]) }
def format_problem_lines(problems) def format_problem_lines(problems)
problems.map do |problem| problems.map do |problem|
status = " #{Formatter.success("[corrected]")}" if problem.fetch(:corrected) status = " #{Formatter.success("[corrected]")}" if problem.fetch(:corrected)

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -20,7 +20,7 @@ module Homebrew
class Bottle < AbstractCommand class Bottle < AbstractCommand
include FileUtils include FileUtils
BOTTLE_ERB = <<-EOS.freeze BOTTLE_ERB = T.let(<<-EOS.freeze, String)
bottle do bottle do
<% if [HOMEBREW_BOTTLE_DEFAULT_DOMAIN.to_s, <% if [HOMEBREW_BOTTLE_DEFAULT_DOMAIN.to_s,
"#{HOMEBREW_BOTTLE_DEFAULT_DOMAIN}/bottles"].exclude?(root_url) %> "#{HOMEBREW_BOTTLE_DEFAULT_DOMAIN}/bottles"].exclude?(root_url) %>
@ -39,9 +39,9 @@ module Homebrew
MAXIMUM_STRING_MATCHES = 100 MAXIMUM_STRING_MATCHES = 100
ALLOWABLE_HOMEBREW_REPOSITORY_LINKS = [ ALLOWABLE_HOMEBREW_REPOSITORY_LINKS = T.let([
%r{#{Regexp.escape(HOMEBREW_LIBRARY)}/Homebrew/os/(mac|linux)/pkgconfig}, %r{#{Regexp.escape(HOMEBREW_LIBRARY)}/Homebrew/os/(mac|linux)/pkgconfig},
].freeze ].freeze, T::Array[Regexp])
cmd_args do cmd_args do
description <<~EOS description <<~EOS
@ -110,6 +110,10 @@ module Homebrew
end end
end end
sig {
params(tag: Symbol, digest: T.any(Checksum, String), cellar: T.nilable(T.any(String, Symbol)),
tag_column: Integer, digest_column: Integer).returns(String)
}
def generate_sha256_line(tag, digest, cellar, tag_column, digest_column) def generate_sha256_line(tag, digest, cellar, tag_column, digest_column)
line = "sha256 " line = "sha256 "
tag_column += line.length tag_column += line.length
@ -125,6 +129,7 @@ module Homebrew
%Q(#{line}"#{digest}") %Q(#{line}"#{digest}")
end end
sig { params(bottle: BottleSpecification, root_url_using: T.nilable(String)).returns(String) }
def bottle_output(bottle, root_url_using) def bottle_output(bottle, root_url_using)
cellars = bottle.checksums.filter_map do |checksum| cellars = bottle.checksums.filter_map do |checksum|
cellar = checksum["cellar"] cellar = checksum["cellar"]
@ -153,12 +158,14 @@ module Homebrew
erb.result(erb_binding).gsub(/^\s*$\n/, "") erb.result(erb_binding).gsub(/^\s*$\n/, "")
end end
sig { params(filenames: T::Array[String]).returns(T::Array[T::Hash[String, T.untyped]]) }
def parse_json_files(filenames) def parse_json_files(filenames)
filenames.map do |filename| filenames.map do |filename|
JSON.parse(File.read(filename)) JSON.parse(File.read(filename))
end end
end end
sig { params(json_files: T::Array[T::Hash[String, T.untyped]]).returns(T::Hash[String, T.untyped]) }
def merge_json_files(json_files) def merge_json_files(json_files)
json_files.reduce({}) do |hash, json_file| json_files.reduce({}) do |hash, json_file|
json_file.each_value do |json_hash| json_file.each_value do |json_hash|
@ -172,6 +179,10 @@ module Homebrew
end end
end end
sig {
params(old_keys: T::Array[String], old_bottle_spec: BottleSpecification,
new_bottle_hash: T::Hash[String, T.untyped]).returns(T::Array[T::Array[String]])
}
def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash) def merge_bottle_spec(old_keys, old_bottle_spec, new_bottle_hash)
mismatches = [] mismatches = []
checksums = [] checksums = []
@ -214,16 +225,20 @@ module Homebrew
private private
sig {
params(string: String, keg: Keg, ignores: T::Array[String],
formula_and_runtime_deps_names: T.nilable(T::Array[String])).returns(T::Boolean)
}
def keg_contain?(string, keg, ignores, formula_and_runtime_deps_names = nil) def keg_contain?(string, keg, ignores, formula_and_runtime_deps_names = nil)
@put_string_exists_header, @put_filenames = nil @put_string_exists_header, @put_filenames = nil
print_filename = lambda do |str, filename| print_filename = lambda do |str, filename|
unless @put_string_exists_header unless @put_string_exists_header
opoo "String '#{str}' still exists in these files:" opoo "String '#{str}' still exists in these files:"
@put_string_exists_header = true @put_string_exists_header = T.let(true, T.nilable(T::Boolean))
end end
@put_filenames ||= [] @put_filenames ||= T.let([], T.nilable(T::Array[T.any(String, Pathname)]))
return false if @put_filenames.include?(filename) return false if @put_filenames.include?(filename)
@ -265,6 +280,7 @@ module Homebrew
keg_contain_absolute_symlink_starting_with?(string, keg) || result keg_contain_absolute_symlink_starting_with?(string, keg) || result
end end
sig { params(string: String, keg: Keg).returns(T::Boolean) }
def keg_contain_absolute_symlink_starting_with?(string, keg) def keg_contain_absolute_symlink_starting_with?(string, keg)
absolute_symlinks_start_with_string = [] absolute_symlinks_start_with_string = []
keg.find do |pn| keg.find do |pn|
@ -283,6 +299,7 @@ module Homebrew
!absolute_symlinks_start_with_string.empty? !absolute_symlinks_start_with_string.empty?
end end
sig { params(cellar: T.nilable(T.any(String, Symbol))).returns(T::Boolean) }
def cellar_parameter_needed?(cellar) def cellar_parameter_needed?(cellar)
default_cellars = [ default_cellars = [
Homebrew::DEFAULT_MACOS_CELLAR, Homebrew::DEFAULT_MACOS_CELLAR,
@ -292,6 +309,7 @@ module Homebrew
cellar.present? && default_cellars.exclude?(cellar) cellar.present? && default_cellars.exclude?(cellar)
end end
sig { returns(T.nilable(T::Boolean)) }
def sudo_purge def sudo_purge
return unless ENV["HOMEBREW_BOTTLE_SUDO_PURGE"] return unless ENV["HOMEBREW_BOTTLE_SUDO_PURGE"]
@ -354,6 +372,7 @@ module Homebrew
[gnu_tar(gnu_tar_formula), reproducible_gnutar_args(mtime)].freeze [gnu_tar(gnu_tar_formula), reproducible_gnutar_args(mtime)].freeze
end end
sig { params(formula: T.untyped).returns(T::Array[T.untyped]) }
def formula_ignores(formula) def formula_ignores(formula)
ignores = [] ignores = []
cellar_regex = Regexp.escape(HOMEBREW_CELLAR) cellar_regex = Regexp.escape(HOMEBREW_CELLAR)
@ -384,6 +403,7 @@ module Homebrew
ignores.compact ignores.compact
end end
sig { params(formula: Formula).void }
def bottle_formula(formula) def bottle_formula(formula)
local_bottle_json = args.json? && formula.local_bottle_path.present? local_bottle_json = args.json? && formula.local_bottle_path.present?
@ -453,6 +473,8 @@ module Homebrew
if local_bottle_json if local_bottle_json
bottle_path = formula.local_bottle_path bottle_path = formula.local_bottle_path
return if bottle_path.blank?
local_filename = bottle_path.basename.to_s local_filename = bottle_path.basename.to_s
tab_path = Utils::Bottles.receipt_path(bottle_path) tab_path = Utils::Bottles.receipt_path(bottle_path)
@ -471,6 +493,7 @@ module Homebrew
else else
tar_filename = filename.to_s.sub(/.gz$/, "") tar_filename = filename.to_s.sub(/.gz$/, "")
tar_path = Pathname.pwd/tar_filename tar_path = Pathname.pwd/tar_filename
return if tar_path.blank?
keg = Keg.new(formula.prefix) keg = Keg.new(formula.prefix)
end end
@ -681,6 +704,7 @@ module Homebrew
json_path.write(JSON.pretty_generate(json)) json_path.write(JSON.pretty_generate(json))
end end
sig { returns(T::Hash[String, T.untyped]) }
def merge def merge
bottles_hash = merge_json_files(parse_json_files(args.named)) bottles_hash = merge_json_files(parse_json_files(args.named))
@ -750,7 +774,7 @@ module Homebrew
end end
end end
all_bottle_hash = T.let(nil, T.nilable(Hash)) all_bottle_hash = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
bottle_hash["bottle"]["tags"].each do |tag, tag_hash| bottle_hash["bottle"]["tags"].each do |tag, tag_hash|
filename = ::Bottle::Filename.new( filename = ::Bottle::Filename.new(
formula_name, formula_name,
@ -801,7 +825,7 @@ module Homebrew
checksums = old_checksums(formula, formula_ast, bottle_hash) checksums = old_checksums(formula, formula_ast, bottle_hash)
update_or_add = checksums.nil? ? "add" : "update" update_or_add = checksums.nil? ? "add" : "update"
checksums&.each(&bottle.method(:sha256)) checksums&.each { |checksum| bottle.sha256(checksum) }
output = bottle_output(bottle, args.root_url_using) output = bottle_output(bottle, args.root_url_using)
puts output puts output
@ -835,8 +859,12 @@ module Homebrew
end end
end end
sig {
params(formula: Formula, formula_ast: Utils::AST::FormulaAST,
bottle_hash: T::Hash[String, T.untyped]).returns(T.nilable(T::Array[String]))
}
def old_checksums(formula, formula_ast, bottle_hash) def old_checksums(formula, formula_ast, bottle_hash)
bottle_node = formula_ast.bottle_block bottle_node = T.cast(formula_ast.bottle_block, T.nilable(RuboCop::AST::BlockNode))
return if bottle_node.nil? return if bottle_node.nil?
return [] unless args.keep_old? return [] unless args.keep_old?

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -145,10 +145,10 @@ module Homebrew
old_mirrors = formula_spec.mirrors old_mirrors = formula_spec.mirrors
new_mirrors ||= args.mirror new_mirrors ||= args.mirror
new_mirror ||= determine_mirror(new_url) if new_url.present? && (new_mirror = determine_mirror(new_url))
new_mirrors ||= [new_mirror] if new_mirror.present? new_mirrors ||= [new_mirror]
check_for_mirrors(formula, old_mirrors, new_mirrors)
check_for_mirrors(formula, old_mirrors, new_mirrors) if new_url.present? end
old_hash = formula_spec.checksum&.hexdigest old_hash = formula_spec.checksum&.hexdigest
new_hash = args.sha256 new_hash = args.sha256
@ -190,12 +190,14 @@ module Homebrew
elsif new_url.blank? && new_version.blank? elsif new_url.blank? && new_version.blank?
raise UsageError, "#{formula}: no `--url` or `--version` argument specified!" raise UsageError, "#{formula}: no `--url` or `--version` argument specified!"
else else
new_url ||= PyPI.update_pypi_url(old_url, T.must(new_version)) return unless new_version.present?
new_url ||= PyPI.update_pypi_url(old_url, new_version)
if new_url.blank? if new_url.blank?
new_url = update_url(old_url, old_version, T.must(new_version)) new_url = update_url(old_url, old_version, new_version)
if new_mirrors.blank? && old_mirrors.present? if new_mirrors.blank? && old_mirrors.present?
new_mirrors = old_mirrors.map do |old_mirror| new_mirrors = old_mirrors.map do |old_mirror|
update_url(old_mirror, old_version, T.must(new_version)) update_url(old_mirror, old_version, new_version)
end end
end end
end end
@ -271,9 +273,9 @@ module Homebrew
old_contents = formula.path.read old_contents = formula.path.read
if new_mirrors.present? if new_mirrors.present? && new_url.present?
replacement_pairs << [ replacement_pairs << [
/^( +)(url "#{Regexp.escape(T.must(new_url))}"[^\n]*?\n)/m, /^( +)(url "#{Regexp.escape(new_url)}"[^\n]*?\n)/m,
"\\1\\2\\1mirror \"#{new_mirrors.join("\"\n\\1mirror \"")}\"\n", "\\1\\2\\1mirror \"#{new_mirrors.join("\"\n\\1mirror \"")}\"\n",
] ]
end end
@ -395,6 +397,7 @@ module Homebrew
private private
sig { params(url: String).returns(T.nilable(String)) }
def determine_mirror(url) def determine_mirror(url)
case url case url
when %r{.*ftp\.gnu\.org/gnu.*} when %r{.*ftp\.gnu\.org/gnu.*}
@ -408,6 +411,7 @@ module Homebrew
end end
end end
sig { params(formula: String, old_mirrors: T::Array[String], new_mirrors: T::Array[String]).void }
def check_for_mirrors(formula, old_mirrors, new_mirrors) def check_for_mirrors(formula, old_mirrors, new_mirrors)
return if new_mirrors.present? || old_mirrors.empty? return if new_mirrors.present? || old_mirrors.empty?
@ -427,11 +431,17 @@ module Homebrew
return new_url if (old_version_parts = old_version.split(".")).length < 2 return new_url if (old_version_parts = old_version.split(".")).length < 2
return new_url if (new_version_parts = new_version.split(".")).length != old_version_parts.length return new_url if (new_version_parts = new_version.split(".")).length != old_version_parts.length
partial_old_version = T.must(old_version_parts[0..-2]).join(".") partial_old_version = old_version_parts[0..-2]&.join(".")
partial_new_version = T.must(new_version_parts[0..-2]).join(".") partial_new_version = new_version_parts[0..-2]&.join(".")
return new_url if partial_old_version.blank? || partial_new_version.blank?
new_url.gsub(%r{/(v?)#{Regexp.escape(partial_old_version)}/}, "/\\1#{partial_new_version}/") new_url.gsub(%r{/(v?)#{Regexp.escape(partial_old_version)}/}, "/\\1#{partial_new_version}/")
end end
sig {
params(formula: Formula, new_version: T.nilable(String), url: String,
specs: Float).returns(T::Array[T.untyped])
}
def fetch_resource_and_forced_version(formula, new_version, url, **specs) def fetch_resource_and_forced_version(formula, new_version, url, **specs)
resource = Resource.new resource = Resource.new
resource.url(url, **specs) resource.url(url, **specs)
@ -442,6 +452,7 @@ module Homebrew
[resource.fetch, forced_version] [resource.fetch, forced_version]
end end
sig { params(formula: Formula, contents: T.nilable(String)).returns(String) }
def formula_version(formula, contents = nil) def formula_version(formula, contents = nil)
spec = :stable spec = :stable
name = formula.name name = formula.name
@ -453,17 +464,29 @@ module Homebrew
end end
end end
sig { params(formula: Formula, tap_remote_repo: String).returns(T.nilable(T::Array[String])) }
def check_open_pull_requests(formula, tap_remote_repo) def check_open_pull_requests(formula, tap_remote_repo)
GitHub.check_for_duplicate_pull_requests(formula.name, tap_remote_repo, tap = formula.tap
return if tap.nil?
GitHub.check_for_duplicate_pull_requests(
formula.name, tap_remote_repo,
state: "open", state: "open",
file: formula.path.relative_path_from(formula.tap.path).to_s, file: formula.path.relative_path_from(tap.path).to_s,
quiet: args.quiet?) quiet: args.quiet?
)
end end
sig {
params(formula: Formula, tap_remote_repo: String, version: T.nilable(String), url: T.nilable(String),
tag: T.nilable(String)).void
}
def check_new_version(formula, tap_remote_repo, version: nil, url: nil, tag: nil) def check_new_version(formula, tap_remote_repo, version: nil, url: nil, tag: nil)
if version.nil? if version.nil?
specs = {} specs = {}
specs[:tag] = tag if tag.present? specs[:tag] = tag if tag.present?
return if url.blank?
version = Version.detect(url, **specs).to_s version = Version.detect(url, **specs).to_s
return if version.blank? return if version.blank?
end end
@ -472,9 +495,13 @@ module Homebrew
check_closed_pull_requests(formula, tap_remote_repo, version:) check_closed_pull_requests(formula, tap_remote_repo, version:)
end end
sig { params(formula: Formula, new_version: String).returns(NilClass) }
def check_throttle(formula, new_version) def check_throttle(formula, new_version)
tap = formula.tap
return if tap.nil?
throttled_rate = formula.livecheck.throttle throttled_rate = formula.livecheck.throttle
throttled_rate ||= if (rate = formula.tap.audit_exceptions.dig(:throttled_formulae, formula.name)) throttled_rate ||= if (rate = tap.audit_exceptions.dig(:throttled_formulae, formula.name))
odisabled "throttled_formulae.json", "Livecheck#throttle" odisabled "throttled_formulae.json", "Livecheck#throttle"
rate rate
end end
@ -486,27 +513,41 @@ module Homebrew
odie "#{formula} should only be updated every #{throttled_rate} releases on multiples of #{throttled_rate}" odie "#{formula} should only be updated every #{throttled_rate} releases on multiples of #{throttled_rate}"
end end
sig {
params(formula: Formula, tap_remote_repo: String,
version: T.nilable(String)).returns(T.nilable(T::Array[String]))
}
def check_closed_pull_requests(formula, tap_remote_repo, version:) def check_closed_pull_requests(formula, tap_remote_repo, version:)
tap = formula.tap
return if tap.nil?
# if we haven't already found open requests, try for an exact match across closed requests # if we haven't already found open requests, try for an exact match across closed requests
GitHub.check_for_duplicate_pull_requests(formula.name, tap_remote_repo, GitHub.check_for_duplicate_pull_requests(
formula.name, tap_remote_repo,
version:, version:,
state: "closed", state: "closed",
file: formula.path.relative_path_from(formula.tap.path).to_s, file: formula.path.relative_path_from(tap.path).to_s,
quiet: args.quiet?) quiet: args.quiet?
)
end end
sig { params(formula: Formula, new_formula_version: String).returns(T.nilable(T::Array[String])) }
def alias_update_pair(formula, new_formula_version) def alias_update_pair(formula, new_formula_version)
versioned_alias = formula.aliases.grep(/^.*@\d+(\.\d+)?$/).first versioned_alias = formula.aliases.grep(/^.*@\d+(\.\d+)?$/).first
return if versioned_alias.nil? return if versioned_alias.nil?
name, old_alias_version = versioned_alias.split("@") name, old_alias_version = versioned_alias.split("@")
return if old_alias_version.blank?
new_alias_regex = (old_alias_version.split(".").length == 1) ? /^\d+/ : /^\d+\.\d+/ new_alias_regex = (old_alias_version.split(".").length == 1) ? /^\d+/ : /^\d+\.\d+/
new_alias_version, = *new_formula_version.to_s.match(new_alias_regex) new_alias_version, = *new_formula_version.to_s.match(new_alias_regex)
return if new_alias_version.blank?
return if Version.new(new_alias_version) <= Version.new(old_alias_version) return if Version.new(new_alias_version) <= Version.new(old_alias_version)
[versioned_alias, "#{name}@#{new_alias_version}"] [versioned_alias, "#{name}@#{new_alias_version}"]
end end
sig { params(formula: Formula, alias_rename: T.nilable(T::Array[String]), old_contents: String).void }
def run_audit(formula, alias_rename, old_contents) def run_audit(formula, alias_rename, old_contents)
audit_args = ["--formula"] audit_args = ["--formula"]
audit_args << "--strict" if args.strict? audit_args << "--strict" if args.strict?
@ -521,7 +562,9 @@ module Homebrew
end end
return return
end end
FileUtils.mv alias_rename.first, alias_rename.last if alias_rename.present? if alias_rename && (source = alias_rename.first) && (destination = alias_rename.last)
FileUtils.mv source, destination
end
failed_audit = false failed_audit = false
if args.no_audit? if args.no_audit?
ohai "Skipping `brew audit`" ohai "Skipping `brew audit`"
@ -535,7 +578,9 @@ module Homebrew
return unless failed_audit return unless failed_audit
formula.path.atomic_write(old_contents) formula.path.atomic_write(old_contents)
FileUtils.mv alias_rename.last, alias_rename.first if alias_rename.present? if alias_rename && (source = alias_rename.first) && (destination = alias_rename.last)
FileUtils.mv source, destination
end
odie "`brew audit` failed!" odie "`brew audit` failed!"
end end
end end

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -10,12 +10,12 @@ end
module Homebrew module Homebrew
module DevCmd module DevCmd
class Contributions < AbstractCommand class Contributions < AbstractCommand
PRIMARY_REPOS = %w[brew core cask].freeze PRIMARY_REPOS = T.let(%w[brew core cask].freeze, T::Array[String])
SUPPORTED_REPOS = [ SUPPORTED_REPOS = T.let([
PRIMARY_REPOS, PRIMARY_REPOS,
OFFICIAL_CMD_TAPS.keys.map { |t| t.delete_prefix("homebrew/") }, OFFICIAL_CMD_TAPS.keys.map { |t| t.delete_prefix("homebrew/") },
OFFICIAL_CASK_TAPS.reject { |t| t == "cask" }, OFFICIAL_CASK_TAPS.reject { |t| t == "cask" },
].flatten.freeze ].flatten.freeze, T::Array[String])
MAX_REPO_COMMITS = 1000 MAX_REPO_COMMITS = 1000
cmd_args do cmd_args do
@ -50,9 +50,9 @@ module Homebrew
results = {} results = {}
grand_totals = {} grand_totals = {}
repos = if args.repositories.blank? || T.must(args.repositories).include?("primary") repos = if args.repositories.blank? || args.repositories&.include?("primary")
PRIMARY_REPOS PRIMARY_REPOS
elsif T.must(args.repositories).include?("all") elsif args.repositories&.include?("all")
SUPPORTED_REPOS SUPPORTED_REPOS
else else
args.repositories args.repositories
@ -116,7 +116,7 @@ module Homebrew
end end
end end
sig { params(totals: Hash).returns(String) } sig { params(totals: T::Hash[String, T::Hash[Symbol, Integer]]).returns(String) }
def generate_csv(totals) def generate_csv(totals)
CSV.generate do |csv| CSV.generate do |csv|
csv << %w[user repo author committer coauthor review total] csv << %w[user repo author committer coauthor review total]
@ -127,7 +127,14 @@ module Homebrew
end end
end end
sig { params(user: String, grand_total: Hash).returns(Array) } sig {
params(
user: String,
grand_total: T::Hash[Symbol, Integer],
).returns(
[String, String, T.nilable(Integer), T.nilable(Integer), T.nilable(Integer), T.nilable(Integer), Integer],
)
}
def grand_total_row(user, grand_total) def grand_total_row(user, grand_total)
[ [
user, user,
@ -140,7 +147,10 @@ module Homebrew
] ]
end end
sig { params(repos: T.nilable(T::Array[String]), person: String, from: String).void }
def scan_repositories(repos, person, from:) def scan_repositories(repos, person, from:)
return if repos.blank?
data = {} data = {}
repos.each do |repo| repos.each do |repo|
@ -168,7 +178,7 @@ module Homebrew
data[repo] = { data[repo] = {
author: author_commits, author: author_commits,
committer: committer_commits, committer: committer_commits,
coauthor: git_log_trailers_cmd(T.must(repo_path), person, "Co-authored-by", from:, to: args.to), coauthor: git_log_trailers_cmd(repo_path, person, "Co-authored-by", from:, to: args.to),
review: count_reviews(repo_full_name, person, from:, to: args.to), review: count_reviews(repo_full_name, person, from:, to: args.to),
} }
end end
@ -176,7 +186,7 @@ module Homebrew
data data
end end
sig { params(results: Hash).returns(Hash) } sig { params(results: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, Integer]) }
def total(results) def total(results)
totals = { author: 0, committer: 0, coauthor: 0, review: 0 } totals = { author: 0, committer: 0, coauthor: 0, review: 0 }

View File

@ -1,4 +1,4 @@
# typed: true # typed: true # This cannot be `# typed: strict` due to the use of `undef`.
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -71,6 +71,7 @@ module Homebrew
private private
sig { params(title: String).returns(String) }
def html_template(title) def html_template(title)
<<~EOS <<~EOS
--- ---

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -69,6 +69,7 @@ module Homebrew
private private
sig { params(title: String).returns(String) }
def html_template(title) def html_template(title)
<<~EOS <<~EOS
--- ---

View File

@ -1,18 +1,20 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
require "formula"
require "formulary" require "formulary"
require "cask/cask_loader" require "cask/cask_loader"
class String class String
# @!visibility private # @!visibility private
sig { params(args: Integer).returns(Formula) }
def f(*args) def f(*args)
require "formula"
Formulary.factory(self, *args) Formulary.factory(self, *args)
end end
# @!visibility private # @!visibility private
sig { params(config: T.nilable(T::Hash[Symbol, T.untyped])).returns(Cask::Cask) }
def c(config: nil) def c(config: nil)
Cask::CaskLoader.load(self, config:) Cask::CaskLoader.load(self, config:)
end end
@ -20,11 +22,13 @@ end
class Symbol class Symbol
# @!visibility private # @!visibility private
sig { params(args: Integer).returns(Formula) }
def f(*args) def f(*args)
to_s.f(*args) to_s.f(*args)
end end
# @!visibility private # @!visibility private
sig { params(config: T.nilable(T::Hash[Symbol, T.untyped])).returns(Cask::Cask) }
def c(config: nil) def c(config: nil)
to_s.c(config:) to_s.c(config:)
end end
@ -72,7 +76,6 @@ module Homebrew
require "irb" require "irb"
end end
require "formula"
require "keg" require "keg"
require "cask" require "cask"
@ -94,6 +97,7 @@ module Homebrew
# Remove the `--debug`, `--verbose` and `--quiet` options which cause problems # Remove the `--debug`, `--verbose` and `--quiet` options which cause problems
# for IRB and have already been parsed by the CLI::Parser. # for IRB and have already been parsed by the CLI::Parser.
sig { returns(T.nilable(T::Array[Symbol])) }
def clean_argv def clean_argv
global_options = Homebrew::CLI::Parser global_options = Homebrew::CLI::Parser
.global_options .global_options

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -113,8 +113,9 @@ module Homebrew
private private
sig { returns(String) }
def watchlist_path def watchlist_path
@watchlist_path ||= File.expand_path(Homebrew::EnvConfig.livecheck_watchlist) @watchlist_path ||= T.let(File.expand_path(Homebrew::EnvConfig.livecheck_watchlist), T.nilable(String))
end end
end end
end end

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -141,7 +141,7 @@ module Homebrew
user, repo, pr, workflow_id: workflow, artifact_pattern: user, repo, pr, workflow_id: workflow, artifact_pattern:
) )
if args.ignore_missing_artifacts.present? && if args.ignore_missing_artifacts.present? &&
T.must(args.ignore_missing_artifacts).include?(workflow) && args.ignore_missing_artifacts&.include?(workflow) &&
workflow_run.first.blank? workflow_run.first.blank?
# Ignore that workflow as it was not executed and we specified # Ignore that workflow as it was not executed and we specified
# that we could skip it. # that we could skip it.
@ -183,8 +183,10 @@ module Homebrew
end end
# Separates a commit message into subject, body and trailers. # Separates a commit message into subject, body and trailers.
sig { params(message: String).returns([String, String, String]) }
def separate_commit_message(message) def separate_commit_message(message)
subject = message.lines.first.strip first_line = message.lines.first
return ["", "", ""] unless first_line
# Skip the subject and separate lines that look like trailers (e.g. "Co-authored-by") # Skip the subject and separate lines that look like trailers (e.g. "Co-authored-by")
# from lines that look like regular body text. # from lines that look like regular body text.
@ -193,11 +195,15 @@ module Homebrew
trailers = trailers.uniq.join.strip trailers = trailers.uniq.join.strip
body = body.join.strip.gsub(/\n{3,}/, "\n\n") body = body.join.strip.gsub(/\n{3,}/, "\n\n")
[subject, body, trailers] [first_line.strip, body, trailers]
end end
sig { params(git_repo: GitRepository, pull_request: T.nilable(String), dry_run: T::Boolean).void }
def signoff!(git_repo, pull_request: nil, dry_run: false) def signoff!(git_repo, pull_request: nil, dry_run: false)
subject, body, trailers = separate_commit_message(git_repo.commit_message) msg = git_repo.commit_message
return if msg.blank?
subject, body, trailers = separate_commit_message(msg)
if pull_request if pull_request
# This is a tap pull request and approving reviewers should also sign-off. # This is a tap pull request and approving reviewers should also sign-off.
@ -210,7 +216,7 @@ module Homebrew
# Append the close message as well, unless the commit body already includes it. # Append the close message as well, unless the commit body already includes it.
close_message = "Closes ##{pull_request}." close_message = "Closes ##{pull_request}."
body += "\n\n#{close_message}" unless body.include? close_message body.concat("\n\n#{close_message}") unless body.include?(close_message)
end end
git_args = Utils::Git.git, "-C", git_repo.pathname, "commit", "--amend", "--signoff", "--allow-empty", git_args = Utils::Git.git, "-C", git_repo.pathname, "commit", "--amend", "--signoff", "--allow-empty",
@ -223,6 +229,7 @@ module Homebrew
end end
end end
sig { params(tap: Tap, subject_name: String, subject_path: Pathname, content: String).returns(T.untyped) }
def get_package(tap, subject_name, subject_path, content) def get_package(tap, subject_name, subject_path, content)
if subject_path.to_s.start_with?("#{tap.cask_dir}/") if subject_path.to_s.start_with?("#{tap.cask_dir}/")
cask = begin cask = begin
@ -240,6 +247,10 @@ module Homebrew
end end
end end
sig {
params(old_contents: String, new_contents: String, subject_path: T.any(String, Pathname),
reason: T.nilable(String)).returns(String)
}
def determine_bump_subject(old_contents, new_contents, subject_path, reason: nil) def determine_bump_subject(old_contents, new_contents, subject_path, reason: nil)
subject_path = Pathname(subject_path) subject_path = Pathname(subject_path)
tap = Tap.from_path(subject_path) tap = Tap.from_path(subject_path)
@ -268,6 +279,10 @@ module Homebrew
# Cherry picks a single commit that modifies a single file. # Cherry picks a single commit that modifies a single file.
# Potentially rewords this commit using {determine_bump_subject}. # Potentially rewords this commit using {determine_bump_subject}.
sig {
params(commit: String, file: String, git_repo: GitRepository, reason: T.nilable(String), verbose: T::Boolean,
resolve: T::Boolean).void
}
def reword_package_commit(commit, file, git_repo:, reason: "", verbose: false, resolve: false) def reword_package_commit(commit, file, git_repo:, reason: "", verbose: false, resolve: false)
package_file = git_repo.pathname / file package_file = git_repo.pathname / file
package_name = package_file.basename.to_s.chomp(".rb") package_name = package_file.basename.to_s.chomp(".rb")
@ -279,7 +294,10 @@ module Homebrew
new_package = Utils::Git.file_at_commit(git_repo.to_s, file, "HEAD") new_package = Utils::Git.file_at_commit(git_repo.to_s, file, "HEAD")
bump_subject = determine_bump_subject(old_package, new_package, package_file, reason:).strip bump_subject = determine_bump_subject(old_package, new_package, package_file, reason:).strip
subject, body, trailers = separate_commit_message(git_repo.commit_message) msg = git_repo.commit_message
return if msg.blank?
subject, body, trailers = separate_commit_message(msg)
if subject != bump_subject && !subject.start_with?("#{package_name}:") if subject != bump_subject && !subject.start_with?("#{package_name}:")
safe_system("git", "-C", git_repo.pathname, "commit", "--amend", "-q", safe_system("git", "-C", git_repo.pathname, "commit", "--amend", "-q",
@ -293,6 +311,10 @@ module Homebrew
# Cherry picks multiple commits that each modify a single file. # Cherry picks multiple commits that each modify a single file.
# Words the commit according to {determine_bump_subject} with the body # Words the commit according to {determine_bump_subject} with the body
# corresponding to all the original commit messages combined. # corresponding to all the original commit messages combined.
sig {
params(commits: T::Array[String], file: String, git_repo: GitRepository, reason: T.nilable(String),
verbose: T::Boolean, resolve: T::Boolean).void
}
def squash_package_commits(commits, file, git_repo:, reason: "", verbose: false, resolve: false) def squash_package_commits(commits, file, git_repo:, reason: "", verbose: false, resolve: false)
odebug "Squashing #{file}: #{commits.join " "}" odebug "Squashing #{file}: #{commits.join " "}"
@ -304,7 +326,10 @@ module Homebrew
messages = [] messages = []
trailers = [] trailers = []
commits.each do |commit| commits.each do |commit|
subject, body, trailer = separate_commit_message(git_repo.commit_message(commit)) msg = git_repo.commit_message(commit)
next if msg.blank?
subject, body, trailer = separate_commit_message(msg)
body = body.lines.map { |line| " #{line.strip}" }.join("\n") body = body.lines.map { |line| " #{line.strip}" }.join("\n")
messages << "* #{subject}\n#{body}".strip messages << "* #{subject}\n#{body}".strip
trailers << trailer trailers << trailer
@ -340,9 +365,12 @@ module Homebrew
end end
# TODO: fix test in `test/dev-cmd/pr-pull_spec.rb` and assume `cherry_picked: false`. # TODO: fix test in `test/dev-cmd/pr-pull_spec.rb` and assume `cherry_picked: false`.
sig {
params(original_commit: String, tap: Tap, reason: T.nilable(String), verbose: T::Boolean, resolve: T::Boolean,
cherry_picked: T::Boolean).void
}
def autosquash!(original_commit, tap:, reason: "", verbose: false, resolve: false, cherry_picked: true) def autosquash!(original_commit, tap:, reason: "", verbose: false, resolve: false, cherry_picked: true)
git_repo = tap.git_repository git_repo = tap.git_repository
original_head = git_repo.head_ref
commits = Utils.safe_popen_read("git", "-C", tap.path, "rev-list", commits = Utils.safe_popen_read("git", "-C", tap.path, "rev-list",
"--reverse", "#{original_commit}..HEAD").lines.map(&:strip) "--reverse", "#{original_commit}..HEAD").lines.map(&:strip)
@ -402,14 +430,18 @@ module Homebrew
end end
end end
rescue rescue
original_head = git_repo&.head_ref
return if original_head.nil?
opoo "Autosquash encountered an error; resetting to original state at #{original_head}" opoo "Autosquash encountered an error; resetting to original state at #{original_head}"
system "git", "-C", tap.path, "reset", "--hard", original_head system "git", "-C", tap.path.to_s, "reset", "--hard", original_head
system "git", "-C", tap.path, "cherry-pick", "--abort" if cherry_picked system "git", "-C", tap.path.to_s, "cherry-pick", "--abort" if cherry_picked
raise raise
end end
private private
sig { params(user: String, repo: String, pull_request: String, path: T.any(String, Pathname)).void }
def cherry_pick_pr!(user, repo, pull_request, path: ".") def cherry_pick_pr!(user, repo, pull_request, path: ".")
if args.dry_run? if args.dry_run?
puts <<~EOS puts <<~EOS
@ -427,6 +459,7 @@ module Homebrew
resolve: args.resolve?) resolve: args.resolve?)
end end
sig { params(tap: Tap, original_commit: String, labels: T::Array[String]).returns(T::Boolean) }
def formulae_need_bottles?(tap, original_commit, labels) def formulae_need_bottles?(tap, original_commit, labels)
return false if args.dry_run? return false if args.dry_run?
@ -437,6 +470,7 @@ module Homebrew
end end
end end
sig { params(tap: Tap, original_commit: String).returns(T::Array[String]) }
def changed_packages(tap, original_commit) def changed_packages(tap, original_commit)
formulae = Utils.popen_read("git", "-C", tap.path, "diff-tree", formulae = Utils.popen_read("git", "-C", tap.path, "diff-tree",
"-r", "--name-only", "--diff-filter=AM", "-r", "--name-only", "--diff-filter=AM",
@ -473,6 +507,7 @@ module Homebrew
formulae + casks formulae + casks
end end
sig { params(repo: String, pull_request: String).void }
def pr_check_conflicts(repo, pull_request) def pr_check_conflicts(repo, pull_request)
long_build_pr_files = GitHub.issues( long_build_pr_files = GitHub.issues(
repo:, state: "open", labels: "no long build conflict", repo:, state: "open", labels: "no long build conflict",

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -120,6 +120,7 @@ module Homebrew
private private
sig { params(bottles_hash: T::Hash[String, T.untyped]).void }
def check_bottled_formulae!(bottles_hash) def check_bottled_formulae!(bottles_hash)
bottles_hash.each do |name, bottle_hash| bottles_hash.each do |name, bottle_hash|
formula_path = HOMEBREW_REPOSITORY/bottle_hash["formula"]["path"] formula_path = HOMEBREW_REPOSITORY/bottle_hash["formula"]["path"]
@ -131,22 +132,25 @@ module Homebrew
end end
end end
sig { params(bottles_hash: T::Hash[String, T.untyped]).returns(T::Boolean) }
def github_releases?(bottles_hash) def github_releases?(bottles_hash)
@github_releases ||= bottles_hash.values.all? do |bottle_hash| @github_releases ||= T.let(bottles_hash.values.all? do |bottle_hash|
root_url = bottle_hash["bottle"]["root_url"] root_url = bottle_hash["bottle"]["root_url"]
url_match = root_url.match GitHubReleases::URL_REGEX url_match = root_url.match GitHubReleases::URL_REGEX
_, _, _, tag = *url_match _, _, _, tag = *url_match
tag tag
end end, T.nilable(T::Boolean))
end end
sig { params(bottles_hash: T::Hash[String, T.untyped]).returns(T::Boolean) }
def github_packages?(bottles_hash) def github_packages?(bottles_hash)
@github_packages ||= bottles_hash.values.all? do |bottle_hash| @github_packages ||= T.let(bottles_hash.values.all? do |bottle_hash|
bottle_hash["bottle"]["root_url"].match? GitHubPackages::URL_REGEX bottle_hash["bottle"]["root_url"].match? GitHubPackages::URL_REGEX
end end, T.nilable(T::Boolean))
end end
sig { params(json_files: T::Array[String], args: T.untyped).returns(T::Hash[String, T.untyped]) }
def bottles_hash_from_json_files(json_files, args) def bottles_hash_from_json_files(json_files, args)
puts "Reading JSON files: #{json_files.join(", ")}" if args.verbose? puts "Reading JSON files: #{json_files.join(", ")}" if args.verbose?

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -197,6 +197,7 @@ module Homebrew
private private
sig { params(tap: Tap, filename: T.any(String, Pathname), content: String).void }
def write_path(tap, filename, content) def write_path(tap, filename, content)
path = tap.path/filename path = tap.path/filename
tap.path.mkpath tap.path.mkpath

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -110,8 +110,9 @@ module Homebrew
private private
sig { params(formula: Formula).returns(T::Boolean) }
def retry_test?(formula) def retry_test?(formula)
@test_failed ||= Set.new @test_failed ||= T.let(Set.new, T.nilable(T::Set[T.untyped]))
if args.retry? && @test_failed.add?(formula) if args.retry? && @test_failed.add?(formula)
oh1 "Testing #{formula.full_name} (again)" oh1 "Testing #{formula.full_name} (again)"
formula.clear_cache formula.clear_cache

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -168,15 +168,17 @@ module Homebrew
private private
sig { returns(T.nilable(T::Boolean)) }
def use_buildpulse? def use_buildpulse?
return @use_buildpulse if defined?(@use_buildpulse) return @use_buildpulse if defined?(@use_buildpulse)
@use_buildpulse = ENV["HOMEBREW_BUILDPULSE_ACCESS_KEY_ID"].present? && @use_buildpulse = T.let(ENV["HOMEBREW_BUILDPULSE_ACCESS_KEY_ID"].present? &&
ENV["HOMEBREW_BUILDPULSE_SECRET_ACCESS_KEY"].present? && ENV["HOMEBREW_BUILDPULSE_SECRET_ACCESS_KEY"].present? &&
ENV["HOMEBREW_BUILDPULSE_ACCOUNT_ID"].present? && ENV["HOMEBREW_BUILDPULSE_ACCOUNT_ID"].present? &&
ENV["HOMEBREW_BUILDPULSE_REPOSITORY_ID"].present? ENV["HOMEBREW_BUILDPULSE_REPOSITORY_ID"].present?, T.nilable(T::Boolean))
end end
sig { void }
def run_buildpulse def run_buildpulse
require "formula" require "formula"
@ -198,6 +200,7 @@ module Homebrew
] ]
end end
sig { returns(T::Array[String]) }
def changed_test_files def changed_test_files
changed_files = Utils.popen_read("git", "diff", "--name-only", "master") changed_files = Utils.popen_read("git", "diff", "--name-only", "master")
@ -215,6 +218,7 @@ module Homebrew
end.select(&:exist?) end.select(&:exist?)
end end
sig { returns(T::Array[String]) }
def setup_environment! def setup_environment!
# Cleanup any unwanted user configuration. # Cleanup any unwanted user configuration.
allowed_test_env = %w[ allowed_test_env = %w[

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -34,11 +34,15 @@ module Homebrew
def run def run
Formulary.enable_factory_cache! Formulary.enable_factory_cache!
@bottle_tag = if (tag = args.tag) @bottle_tag = T.let(
if (tag = args.tag)
Utils::Bottles::Tag.from_symbol(tag.to_sym) Utils::Bottles::Tag.from_symbol(tag.to_sym)
else else
Utils::Bottles.tag Utils::Bottles.tag
end end,
T.nilable(Utils::Bottles::Tag),
)
return unless @bottle_tag
if args.lost? if args.lost?
if args.named.present? if args.named.present?
@ -98,12 +102,17 @@ module Homebrew
["installs", formula_installs] ["installs", formula_installs]
end end
return if hash.nil?
output_unbottled(formulae, deps_hash, noun, hash, args.named.present?) output_unbottled(formulae, deps_hash, noun, hash, args.named.present?)
end end
end end
private private
sig {
params(all: T::Boolean).returns([T::Array[Formula], T::Array[Formula], T.nilable(T::Hash[Symbol, Integer])])
}
def formulae_all_installs_from_args(all) def formulae_all_installs_from_args(all)
if args.named.present? if args.named.present?
formulae = all_formulae = args.named.to_formulae formulae = all_formulae = args.named.to_formulae
@ -115,7 +124,7 @@ module Homebrew
formulae = all_formulae = Formula.all(eval_all: args.eval_all?) formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
@sort = " (sorted by number of dependents)" @sort = T.let(" (sorted by number of dependents)", T.nilable(String))
elsif all elsif all
formulae = all_formulae = Formula.all(eval_all: args.eval_all?) formulae = all_formulae = Formula.all(eval_all: args.eval_all?)
else else
@ -142,7 +151,7 @@ module Homebrew
nil nil
end end
end end
@sort = " (sorted by installs in the last 90 days; top 10,000 only)" @sort = T.let(" (sorted by installs in the last 90 days; top 10,000 only)", T.nilable(String))
all_formulae = Formula.all(eval_all: args.eval_all?) all_formulae = Formula.all(eval_all: args.eval_all?)
end end
@ -151,9 +160,11 @@ module Homebrew
formulae = Array(formulae).reject(&:deprecated?) if formulae.present? formulae = Array(formulae).reject(&:deprecated?) if formulae.present?
all_formulae = Array(all_formulae).reject(&:deprecated?) if all_formulae.present? all_formulae = Array(all_formulae).reject(&:deprecated?) if all_formulae.present?
[formulae, all_formulae, formula_installs] [T.let(formulae, T::Array[Formula]), T.let(all_formulae, T::Array[Formula]),
T.let(formula_installs, T.nilable(T::Hash[Symbol, Integer]))]
end end
sig { params(all_formulae: T.untyped).returns([T::Hash[String, T.untyped], T::Hash[String, T.untyped]]) }
def deps_uses_from_formulae(all_formulae) def deps_uses_from_formulae(all_formulae)
ohai "Populating dependency tree..." ohai "Populating dependency tree..."
@ -175,7 +186,10 @@ module Homebrew
[deps_hash, uses_hash] [deps_hash, uses_hash]
end end
sig { params(formulae: T::Array[Formula]).returns(NilClass) }
def output_total(formulae) def output_total(formulae)
return unless @bottle_tag
ohai "Unbottled :#{@bottle_tag} formulae" ohai "Unbottled :#{@bottle_tag} formulae"
unbottled_formulae = 0 unbottled_formulae = 0
@ -188,7 +202,14 @@ module Homebrew
puts "#{unbottled_formulae}/#{formulae.length} remaining." puts "#{unbottled_formulae}/#{formulae.length} remaining."
end end
sig {
params(formulae: T::Array[Formula], deps_hash: T::Hash[T.any(Symbol, String), T.untyped],
noun: T.nilable(String), hash: T::Hash[T.any(Symbol, String), T.untyped],
any_named_args: T::Boolean).returns(NilClass)
}
def output_unbottled(formulae, deps_hash, noun, hash, any_named_args) def output_unbottled(formulae, deps_hash, noun, hash, any_named_args)
return unless @bottle_tag
ohai ":#{@bottle_tag} bottle status#{@sort}" ohai ":#{@bottle_tag} bottle status#{@sort}"
any_found = T.let(false, T::Boolean) any_found = T.let(false, T::Boolean)
@ -258,6 +279,7 @@ module Homebrew
puts "No unbottled dependencies found!" puts "No unbottled dependencies found!"
end end
sig { returns(NilClass) }
def output_lost_bottles def output_lost_bottles
ohai ":#{@bottle_tag} lost bottles" ohai ":#{@bottle_tag} lost bottles"

View File

@ -1,4 +1,4 @@
# typed: true # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "abstract_command" require "abstract_command"
@ -62,14 +62,17 @@ module Homebrew
private private
sig { params(sponsor: T::Hash[Symbol, T.untyped]).returns(T.nilable(String)) }
def sponsor_name(sponsor) def sponsor_name(sponsor)
sponsor[:name] || sponsor[:login] sponsor[:name] || sponsor[:login]
end end
sig { params(sponsor: T::Hash[Symbol, T.untyped]).returns(String) }
def sponsor_logo(sponsor) def sponsor_logo(sponsor)
"https://github.com/#{sponsor[:login]}.png?size=64" "https://github.com/#{sponsor[:login]}.png?size=64"
end end
sig { params(sponsor: T::Hash[Symbol, T.untyped]).returns(String) }
def sponsor_url(sponsor) def sponsor_url(sponsor)
"https://github.com/#{sponsor[:login]}" "https://github.com/#{sponsor[:login]}"
end end