Enable strict typing in Formulary

This commit is contained in:
Rylan Polster 2025-08-24 12:58:48 -04:00
parent 566290dcbc
commit 4410388043
No known key found for this signature in database
2 changed files with 164 additions and 73 deletions

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "digest/sha2"
@ -34,23 +34,41 @@ module Formulary
# @api internal
sig { void }
def self.enable_factory_cache!
@factory_cache = true
@factory_cache_enabled = T.let(true, T.nilable(T::Boolean))
cache[platform_cache_tag] ||= {}
cache[platform_cache_tag][:formulary_factory] ||= {}
end
sig { returns(T::Boolean) }
def self.factory_cached?
!@factory_cache.nil?
!@factory_cache_enabled.nil?
end
sig { returns(String) }
def self.platform_cache_tag
"#{Homebrew::SimulateSystem.current_os}_#{Homebrew::SimulateSystem.current_arch}"
end
private_class_method :platform_cache_tag
sig { returns(T::Hash[Symbol, T::Hash[String, T.class_of(Formula)]]) }
def self.platform_cache
cache["#{Homebrew::SimulateSystem.current_os}_#{Homebrew::SimulateSystem.current_arch}"] ||= {}
cache[platform_cache_tag] ||= {}
end
sig { returns(T::Hash[String, Formula]) }
def self.factory_cache
cache[platform_cache_tag] ||= {}
cache[platform_cache_tag][:formulary_factory] ||= {}
end
sig { params(path: T.any(String, Pathname)).returns(T::Boolean) }
def self.formula_class_defined_from_path?(path)
platform_cache.key?(:path) && platform_cache[:path].key?(path)
platform_cache.key?(:path) && platform_cache.fetch(:path).key?(path.to_s)
end
sig { params(name: String).returns(T::Boolean) }
def self.formula_class_defined_from_api?(name)
platform_cache.key?(:api) && platform_cache[:api].key?(name)
platform_cache.key?(:api) && platform_cache.fetch(:api).key?(name)
end
sig { params(name: String).returns(T::Boolean) }
@ -58,12 +76,14 @@ module Formulary
platform_cache.key?(:stub) && platform_cache.fetch(:stub).key?(name)
end
sig { params(path: T.any(String, Pathname)).returns(T.class_of(Formula)) }
def self.formula_class_get_from_path(path)
platform_cache[:path].fetch(path)
platform_cache.fetch(:path).fetch(path.to_s)
end
sig { params(name: String).returns(T.class_of(Formula)) }
def self.formula_class_get_from_api(name)
platform_cache[:api].fetch(name)
platform_cache.fetch(:api).fetch(name)
end
sig { params(name: String).returns(T.class_of(Formula)) }
@ -71,6 +91,7 @@ module Formulary
platform_cache.fetch(:stub).fetch(name)
end
sig { void }
def self.clear_cache
platform_cache.each do |type, cached_objects|
next if type == :formulary_factory
@ -92,10 +113,10 @@ module Formulary
end
module PathnameWriteMkpath
# TODO: migrate away from refinements here, they don't play nicely with
# Sorbet, when we migrate to `typed: strict`
# TODO: migrate away from refinements here, they don't play nicely with Sorbet
# rubocop:todo Sorbet/BlockMethodDefinition
refine Pathname do
sig { params(content: Object, offset: T.nilable(Integer), open_args: T.untyped).returns(Integer) }
def write(content, offset = nil, **open_args)
T.bind(self, Pathname)
raise "Will not overwrite #{self}" if exist? && !offset && !open_args[:mode]&.match?(/^a\+?$/)
@ -109,6 +130,17 @@ module Formulary
end
using PathnameWriteMkpath
sig {
params(
name: String,
path: Pathname,
contents: String,
namespace: String,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(T.class_of(Formula))
}
def self.load_formula(name, path, contents, namespace, flags:, ignore_errors:)
raise "Formula loading disabled by `$HOMEBREW_DISABLE_LOAD_FORMULA`!" if Homebrew::EnvConfig.disable_load_formula?
@ -121,6 +153,7 @@ module Formulary
$stdout = StringIO.new
mod = Module.new
namespace = namespace.to_sym
remove_const(namespace) if const_defined?(namespace)
const_set(namespace, mod)
@ -128,7 +161,7 @@ module Formulary
# Set `BUILD_FLAGS` in the formula's namespace so we can
# access them from within the formula's class scope.
mod.const_set(:BUILD_FLAGS, flags)
mod.module_eval(contents, path)
mod.module_eval(contents, path.to_s)
rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersion::Error => e
if e.is_a?(Ignorable::ExceptionMixin)
e.ignore
@ -174,6 +207,13 @@ module Formulary
)
end
sig { params(string: String).returns(String) }
def self.replace_placeholders(string)
string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
end
sig {
params(name: String, path: Pathname, flags: T::Array[String], ignore_errors: T::Boolean)
.returns(T.class_of(Formula))
@ -183,7 +223,7 @@ module Formulary
namespace = "FormulaNamespace#{namespace_key(path.to_s)}"
klass = load_formula(name, path, contents, namespace, flags:, ignore_errors:)
platform_cache[:path] ||= {}
platform_cache[:path][path] = klass
platform_cache.fetch(:path)[path.to_s] = klass
end
sig { params(name: String, json_formula_with_variations: T::Hash[String, T.untyped], flags: T::Array[String]).returns(T.class_of(Formula)) }
@ -199,6 +239,8 @@ module Formulary
class_name = class_s(name)
json_formula = Homebrew::API.merge_variations(json_formula_with_variations)
caveats_string = (replace_placeholders(json_formula["caveats"]) if json_formula["caveats"])
uses_from_macos_names = json_formula.fetch("uses_from_macos", []).map do |dep|
next dep unless dep.is_a? Hash
@ -278,12 +320,9 @@ module Formulary
end
end
# TODO: migrate away from this inline class here, they don't play nicely with
# Sorbet, when we migrate to `typed: strict`
# rubocop:todo Sorbet/BlockMethodDefinition
klass = Class.new(::Formula) do
@loaded_from_api = true
@api_source = json_formula_with_variations
@loaded_from_api = T.let(true, T.nilable(T::Boolean))
@api_source = T.let(json_formula_with_variations, T.nilable(T::Hash[String, T.untyped]))
desc json_formula["desc"]
homepage json_formula["homepage"]
@ -373,13 +412,13 @@ module Formulary
link_overwrite overwrite_path
end
def install
raise "Cannot build from source from abstract formula."
define_method(:install) do
raise NotImplementedError, "Cannot build from source from abstract formula."
end
@post_install_defined_boolean = json_formula["post_install_defined"]
@post_install_defined_boolean = T.let(json_formula["post_install_defined"], T.nilable(T::Boolean))
@post_install_defined_boolean = true if @post_install_defined_boolean.nil? # Backwards compatibility
def post_install_defined?
define_method(:post_install_defined?) do
self.class.instance_variable_get(:@post_install_defined_boolean)
end
@ -407,55 +446,48 @@ module Formulary
end
end
@caveats_string = json_formula["caveats"]
def caveats
caveats_string = self.class.instance_variable_get(:@caveats_string)
return unless caveats_string
caveats_string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
@caveats_string = T.let(caveats_string, T.nilable(String))
define_method(:caveats) do
self.class.instance_variable_get(:@caveats_string)
end
@tap_git_head_string = json_formula["tap_git_head"]
def tap_git_head
@tap_git_head_string = T.let(json_formula["tap_git_head"], T.nilable(String))
define_method(:tap_git_head) do
self.class.instance_variable_get(:@tap_git_head_string)
end
@oldnames_array = json_formula["oldnames"] || [json_formula["oldname"]].compact
def oldnames
@oldnames_array = T.let(json_formula["oldnames"] || [json_formula["oldname"]].compact, T.nilable(T::Array[String]))
define_method(:oldnames) do
self.class.instance_variable_get(:@oldnames_array)
end
@aliases_array = json_formula.fetch("aliases", [])
def aliases
@aliases_array = T.let(json_formula.fetch("aliases", []), T.nilable(T::Array[String]))
define_method(:aliases) do
self.class.instance_variable_get(:@aliases_array)
end
@versioned_formulae_array = json_formula.fetch("versioned_formulae", [])
def versioned_formulae_names
@versioned_formulae_array = T.let(json_formula.fetch("versioned_formulae", []), T.nilable(T::Array[String]))
define_method(:versioned_formulae_names) do
self.class.instance_variable_get(:@versioned_formulae_array)
end
@ruby_source_path_string = json_formula["ruby_source_path"]
def ruby_source_path
@ruby_source_path_string = T.let(json_formula["ruby_source_path"], T.nilable(String))
define_method(:ruby_source_path) do
self.class.instance_variable_get(:@ruby_source_path_string)
end
@ruby_source_checksum_string = json_formula.dig("ruby_source_checksum", "sha256")
@ruby_source_checksum_string = T.let(json_formula.dig("ruby_source_checksum", "sha256"), T.nilable(String))
@ruby_source_checksum_string ||= json_formula["ruby_source_sha256"]
def ruby_source_checksum
define_method(:ruby_source_checksum) do
checksum = self.class.instance_variable_get(:@ruby_source_checksum_string)
Checksum.new(checksum) if checksum
end
end
# rubocop:enable Sorbet/BlockMethodDefinition
mod.const_set(class_name, klass)
platform_cache[:api] ||= {}
platform_cache[:api][name] = klass
platform_cache.fetch(:api)[name] = klass
end
sig { params(name: String, formula_stub: Homebrew::FormulaStub, flags: T::Array[String]).returns(T.class_of(Formula)) }
@ -471,8 +503,8 @@ module Formulary
class_name = class_s(name)
klass = Class.new(::Formula) do
@loaded_from_api = true
@loaded_from_stub = true
@loaded_from_api = T.let(true, T.nilable(T::Boolean))
@loaded_from_stub = T.let(true, T.nilable(T::Boolean))
url "formula-stub://#{name}/#{formula_stub.pkg_version}"
version formula_stub.version.to_s
@ -496,7 +528,7 @@ module Formulary
mod.const_set(class_name, klass)
platform_cache[:stub] ||= {}
platform_cache[:stub][name] = klass
platform_cache.fetch(:stub)[name] = klass
end
sig {
@ -540,10 +572,12 @@ module Formulary
f
end
sig { params(io: IO).returns(IO) }
def self.ensure_utf8_encoding(io)
io.set_encoding(Encoding::UTF_8)
end
sig { params(name: String).returns(String) }
def self.class_s(name)
class_name = name.capitalize
class_name.gsub!(/[-_.\s]([a-zA-Z0-9])/) { T.must(Regexp.last_match(1)).upcase }
@ -552,8 +586,9 @@ module Formulary
class_name
end
sig { params(string: String).returns(T.any(String, Symbol)) }
def self.convert_to_string_or_symbol(string)
return string[1..].to_sym if string.start_with?(":")
return T.must(string[1..]).to_sym if string.start_with?(":")
string
end
@ -573,14 +608,16 @@ module Formulary
attr_reader :path
# The name used to install the formula.
sig { returns(T.nilable(Pathname)) }
sig { returns(T.nilable(T.any(Pathname, String))) }
attr_reader :alias_path
# The formula's tap (`nil` if it should be implicitly determined).
sig { returns(T.nilable(Tap)) }
attr_reader :tap
sig { params(name: String, path: Pathname, alias_path: T.nilable(Pathname), tap: T.nilable(Tap)).void }
sig {
params(name: String, path: Pathname, alias_path: T.nilable(T.any(Pathname, String)), tap: T.nilable(Tap)).void
}
def initialize(name, path, alias_path: nil, tap: nil)
@name = name
@path = path
@ -591,12 +628,23 @@ module Formulary
# Gets the formula instance.
# `alias_path` can be overridden here in case an alias was used to refer to
# a formula that was loaded in another way.
sig {
overridable.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
alias_path ||= self.alias_path
alias_path = Pathname(alias_path) if alias_path.is_a?(String)
klass(flags:, ignore_errors:)
.new(name, path, spec, alias_path:, tap:, force_bottle:)
end
sig { overridable.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:)
load_file(flags:, ignore_errors:) unless Formulary.formula_class_defined_from_path?(path)
Formulary.formula_class_get_from_path(path)
@ -604,6 +652,7 @@ module Formulary
private
sig { overridable.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:)
raise FormulaUnavailableError, name unless path.file?
@ -627,13 +676,23 @@ module Formulary
new(ref) if HOMEBREW_BOTTLES_EXTNAME_REGEX.match?(ref) && File.exist?(ref)
end
sig { params(bottle_name: String, warn: T::Boolean).void }
def initialize(bottle_name, warn: false)
@bottle_path = Pathname(bottle_name).realpath
@bottle_path = T.let(Pathname(bottle_name).realpath, Pathname)
name, full_name = Utils::Bottles.resolve_formula_names(@bottle_path)
super name, Formulary.path(full_name)
end
def get_formula(spec, force_bottle: false, flags: [], ignore_errors: false, **)
sig {
override.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
formula = begin
contents = Utils::Bottles.formula_contents(@bottle_path, name:)
Formulary.from_contents(name, path, contents, spec, force_bottle:,
@ -736,10 +795,10 @@ module Formulary
return if Homebrew::EnvConfig.forbid_packages_from_paths?
# Cache compiled regex
@uri_regex ||= begin
@uri_regex ||= T.let(begin
uri_regex = ::URI::RFC2396_PARSER.make_regexp
Regexp.new("\\A#{uri_regex.source}\\Z", uri_regex.options)
end
end, T.nilable(Regexp))
uri = ref.to_s
return unless uri.match?(@uri_regex)
@ -751,6 +810,7 @@ module Formulary
new(uri, from:)
end
sig { returns(T.any(URI::Generic, String)) }
attr_reader :url
sig { params(url: T.any(URI::Generic, String), from: T.nilable(Symbol)).void }
@ -764,6 +824,7 @@ module Formulary
super formula, HOMEBREW_CACHE_FORMULA/File.basename(uri_path)
end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:)
url_scheme = URI(url).scheme
if ALLOWED_URL_SCHEMES.exclude?(url_scheme)
@ -777,7 +838,7 @@ module Formulary
Utils::Curl.curl_download url.to_s, to: path
super
rescue MethodDeprecatedError => e
if (match_data = url.match(%r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/}).presence)
if (match_data = url.to_s.match(%r{github.com/(?<user>[\w-]+)/(?<repo>[\w-]+)/}).presence)
e.issues_url = "https://github.com/#{match_data[:user]}/#{match_data[:repo]}/issues/new"
end
raise
@ -794,7 +855,7 @@ module Formulary
sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(FormulaLoader))
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, from: nil, warn: false)
ref = ref.to_s
@ -810,7 +871,7 @@ module Formulary
end
if type == :migration && tap.core_tap? && (loader = FromAPILoader.try_new(name))
loader
T.cast(loader, T.attached_class)
else
new(name, path, tap:, alias_name:)
end
@ -824,6 +885,15 @@ module Formulary
@tap = tap
end
sig {
override.params(
spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
super
rescue FormulaUnreadableError => e
@ -834,6 +904,7 @@ module Formulary
raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace
end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).void }
def load_file(flags:, ignore_errors:)
super
rescue MethodDeprecatedError => e
@ -845,8 +916,8 @@ module Formulary
# Loads a formula from a name, as long as it exists only in a single tap.
class FromNameLoader < FromTapLoader
sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(FormulaLoader))
override.params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, from: nil, warn: false)
return unless ref.is_a?(String)
@ -922,7 +993,16 @@ module Formulary
super name, Formulary.core_path(name)
end
def get_formula(*)
sig {
override.params(
_spec: Symbol,
alias_path: T.nilable(T.any(Pathname, String)),
force_bottle: T::Boolean,
flags: T::Array[String],
ignore_errors: T::Boolean,
).returns(Formula)
}
def get_formula(_spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false)
raise FormulaUnavailableError, name
end
end
@ -930,13 +1010,16 @@ module Formulary
# Load formulae directly from their contents.
class FormulaContentsLoader < FormulaLoader
# The formula's contents.
sig { returns(String) }
attr_reader :contents
sig { params(name: String, path: Pathname, contents: String).void }
def initialize(name, path, contents)
@contents = contents
super name, path
end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:)
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(contents.to_s)}"
Formulary.load_formula(name, path, contents, namespace, flags:, ignore_errors:)
@ -979,6 +1062,7 @@ module Formulary
super(name, Formulary.core_path(name), alias_path:, tap:)
end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:)
load_from_api(flags:) unless Formulary.formula_class_defined_from_api?(name)
Formulary.formula_class_get_from_api(name)
@ -986,6 +1070,7 @@ module Formulary
private
sig { overridable.params(flags: T::Array[String]).void }
def load_from_api(flags:)
json_formula = Homebrew::API::Formula.all_formulae[name]
raise FormulaUnavailableError, name if json_formula.nil?
@ -996,6 +1081,7 @@ module Formulary
# Load formulae directly from their JSON contents.
class FormulaJSONContentsLoader < FromAPILoader
sig { params(name: String, contents: T::Hash[String, T.untyped], tap: T.nilable(Tap), alias_name: T.nilable(String)).void }
def initialize(name, contents, tap: nil, alias_name: nil)
@contents = contents
super(name, tap: tap, alias_name: alias_name)
@ -1003,6 +1089,7 @@ module Formulary
private
sig { override.params(flags: T::Array[String]).void }
def load_from_api(flags:)
Formulary.load_formula_from_json!(name, @contents, flags:)
end
@ -1011,7 +1098,7 @@ module Formulary
# Load a formula stub from the internal API.
class FormulaStubLoader < FromAPILoader
sig {
params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
override.params(ref: T.any(String, Pathname, URI::Generic), from: T.nilable(Symbol), warn: T::Boolean)
.returns(T.nilable(T.attached_class))
}
def self.try_new(ref, from: nil, warn: false)
@ -1020,6 +1107,7 @@ module Formulary
super
end
sig { override.params(flags: T::Array[String], ignore_errors: T::Boolean).returns(T.class_of(Formula)) }
def klass(flags:, ignore_errors:)
load_from_api(flags:) unless Formulary.formula_class_defined_from_stub?(name)
Formulary.formula_class_get_from_stub(name)
@ -1027,6 +1115,7 @@ module Formulary
private
sig { override.params(flags: T::Array[String]).void }
def load_from_api(flags:)
formula_stub = Homebrew::API::Internal.formula_stub(name)
@ -1068,18 +1157,13 @@ module Formulary
prefer_stub: false
)
cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}-#{prefer_stub}"
if factory_cached? && platform_cache[:formulary_factory]&.key?(cache_key)
return platform_cache[:formulary_factory][cache_key]
end
return factory_cache.fetch(cache_key) if factory_cached? && factory_cache.key?(cache_key)
loader = FormulaStubLoader.try_new(ref, from:, warn:) if prefer_stub
loader ||= loader_for(ref, from:, warn:)
formula = loader.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
if factory_cached?
platform_cache[:formulary_factory] ||= {}
platform_cache[:formulary_factory][cache_key] ||= formula
end
factory_cache[cache_key] ||= formula if factory_cached?
formula
end
@ -1118,6 +1202,7 @@ module Formulary
end
# Return whether given rack is keg-only.
sig { params(rack: Pathname).returns(T::Boolean) }
def self.keg_only?(rack)
Formulary.from_rack(rack).keg_only?
rescue FormulaUnavailableError, TapFormulaAmbiguityError
@ -1224,6 +1309,7 @@ module Formulary
.get_formula(spec, alias_path:, force_bottle:, flags:, ignore_errors:)
end
sig { params(ref: String).returns(Pathname) }
def self.to_rack(ref)
# If using a fully-scoped reference, check if the formula can be resolved.
factory(ref) if ref.include? "/"
@ -1237,6 +1323,7 @@ module Formulary
(HOMEBREW_CELLAR/canonical_name(ref)).resolved_path
end
sig { params(ref: String).returns(String) }
def self.canonical_name(ref)
loader_for(ref).name
rescue TapFormulaAmbiguityError
@ -1245,6 +1332,7 @@ module Formulary
ref.downcase
end
sig { params(ref: String).returns(Pathname) }
def self.path(ref)
loader_for(ref).path
end
@ -1293,6 +1381,7 @@ module Formulary
[name, tap, type]
end
sig { params(ref: T.any(String, Pathname), from: T.nilable(Symbol), warn: T::Boolean).returns(FormulaLoader) }
def self.loader_for(ref, from: nil, warn: true)
[
FromBottleLoader,
@ -1303,15 +1392,17 @@ module Formulary
FromNameLoader,
FromKegLoader,
FromCacheLoader,
NullLoader,
].each do |loader_class|
if (loader = loader_class.try_new(ref, from:, warn:))
$stderr.puts "#{$PROGRAM_NAME} (#{loader_class}): loading #{ref}" if verbose? && debug?
return loader
end
end
NullLoader.new(ref)
end
sig { params(name: String).returns(Pathname) }
def self.core_path(name)
find_formula_in_tap(name.to_s.downcase, CoreTap.instance)
end

View File

@ -171,10 +171,10 @@ module Homebrew
log_command = "git log --since='1 month ago' --diff-filter=D " \
"--name-only --max-count=1 " \
"--format=%H\\\\n%h\\\\n%B -- #{relative_path}"
hash, short_hash, *commit_message, relative_path =
hash, short_hash, *commit_message, relative_path_string =
Utils.popen_read(log_command).gsub("\\n", "\n").lines.map(&:chomp)
if hash.blank? || short_hash.blank? || relative_path.blank?
if hash.blank? || short_hash.blank? || relative_path_string.blank?
ofail "No previously deleted formula found." unless silent
return
end
@ -189,7 +189,7 @@ module Homebrew
#{commit_message}
To show the formula before removal, run:
git -C "$(brew --repo #{tap})" show #{short_hash}^:#{relative_path}
git -C "$(brew --repo #{tap})" show #{short_hash}^:#{relative_path_string}
If you still use this formula, consider creating your own tap:
#{Formatter.url("https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap")}