brew/Library/Homebrew/cmd/update-report.rb
Issy Long 45978435e7
rubocop: Use Sorbet/StrictSigil as it's better than comments
- Previously I thought that comments were fine to discourage people from
  wasting their time trying to bump things that used `undef` that Sorbet
  didn't support. But RuboCop is better at this since it'll complain if
  the comments are unnecessary.

- Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501.

- I've gone for a mixture of `rubocop:disable` for the files that can't
  be `typed: strict` (use of undef, required before everything else, etc)
  and `rubocop:todo` for everything else that should be tried to make
  strictly typed. There's no functional difference between the two as
  `rubocop:todo` is `rubocop:disable` with a different name.

- And I entirely disabled the cop for the docs/ directory since
  `typed: strict` isn't going to gain us anything for some Markdown
  linting config files.

- This means that now it's easier to track what needs to be done rather
  than relying on checklists of files in our big Sorbet issue:

```shell
$ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l
    268
```

- And this is confirmed working for new files:

```shell
$ git status
On branch use-rubocop-for-sorbet-strict-sigils
Untracked files:
  (use "git add <file>..." to include in what will be committed)
        Library/Homebrew/bad.rb
        Library/Homebrew/good.rb

nothing added to commit but untracked files present (use "git add" to track)

$ brew style
Offenses:

bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true.
^^^^^^^^^^^^^

1340 files inspected, 1 offense detected
```
2024-08-12 15:24:27 +01:00

877 lines
29 KiB
Ruby

# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "abstract_command"
require "migrator"
require "formulary"
require "cask/cask_loader"
require "cask/migrator"
require "descriptions"
require "cleanup"
require "description_cache_store"
require "settings"
require "linuxbrew-core-migration"
module Homebrew
module Cmd
class UpdateReport < AbstractCommand
cmd_args do
description <<~EOS
The Ruby implementation of `brew update`. Never called manually.
EOS
switch "--auto-update", "--preinstall",
description: "Run in 'auto-update' mode (faster, less output)."
switch "-f", "--force",
description: "Treat installed and updated formulae as if they are from " \
"the same taps and migrate them anyway."
hide_from_man_page!
end
sig { override.void }
def run
return output_update_report if $stdout.tty?
redirect_stdout($stderr) do
output_update_report
end
end
private
def auto_update_header
@auto_update_header ||= begin
ohai "Auto-updated Homebrew!" if args.auto_update?
true
end
end
def output_update_report
# Run `brew update` (again) if we've got a linuxbrew-core CoreTap
if CoreTap.instance.installed? && CoreTap.instance.linuxbrew_core? &&
ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"].blank?
ohai "Re-running `brew update` for linuxbrew-core migration"
if Homebrew::EnvConfig.core_git_remote != HOMEBREW_CORE_DEFAULT_GIT_REMOTE
opoo <<~EOS
HOMEBREW_CORE_GIT_REMOTE was set: #{Homebrew::EnvConfig.core_git_remote}.
It has been unset for the migration.
You may need to change this from a linuxbrew-core mirror to a homebrew-core one.
EOS
end
ENV.delete("HOMEBREW_CORE_GIT_REMOTE")
if Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
opoo <<~EOS
HOMEBREW_BOTTLE_DOMAIN was set: #{Homebrew::EnvConfig.bottle_domain}.
It has been unset for the migration.
You may need to change this from a Linuxbrew package mirror to a Homebrew one.
EOS
end
ENV.delete("HOMEBREW_BOTTLE_DOMAIN")
ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"] = "1"
FileUtils.rm_f HOMEBREW_LOCKS/"update"
update_args = []
update_args << "--auto-update" if args.auto_update?
update_args << "--force" if args.force?
exec HOMEBREW_BREW_FILE, "update", *update_args
end
if ENV["HOMEBREW_ADDITIONAL_GOOGLE_ANALYTICS_ID"].present?
opoo "HOMEBREW_ADDITIONAL_GOOGLE_ANALYTICS_ID is now a no-op so can be unset."
puts "All Homebrew Google Analytics code and data was destroyed."
end
if ENV["HOMEBREW_NO_GOOGLE_ANALYTICS"].present?
opoo "HOMEBREW_NO_GOOGLE_ANALYTICS is now a no-op so can be unset."
puts "All Homebrew Google Analytics code and data was destroyed."
end
unless args.quiet?
analytics_message
donation_message
install_from_api_message
end
tap_or_untap_core_taps_if_necessary
updated = false
new_tag = nil
initial_revision = ENV["HOMEBREW_UPDATE_BEFORE"].to_s
current_revision = ENV["HOMEBREW_UPDATE_AFTER"].to_s
odie "update-report should not be called directly!" if initial_revision.empty? || current_revision.empty?
if initial_revision != current_revision
auto_update_header
updated = true
old_tag = Settings.read "latesttag"
new_tag = Utils.popen_read(
"git", "-C", HOMEBREW_REPOSITORY, "tag", "--list", "--sort=-version:refname", "*.*"
).lines.first.chomp
Settings.write "latesttag", new_tag if new_tag != old_tag
if new_tag == old_tag
ohai "Updated Homebrew from #{shorten_revision(initial_revision)} " \
"to #{shorten_revision(current_revision)}."
elsif old_tag.blank?
ohai "Updated Homebrew from #{shorten_revision(initial_revision)} " \
"to #{new_tag} (#{shorten_revision(current_revision)})."
else
ohai "Updated Homebrew from #{old_tag} (#{shorten_revision(initial_revision)}) " \
"to #{new_tag} (#{shorten_revision(current_revision)})."
end
end
# Check if we can parse the JSON and do any Ruby-side follow-up.
unless Homebrew::EnvConfig.no_install_from_api?
Homebrew::API::Formula.write_names_and_aliases
Homebrew::API::Cask.write_names
end
Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
return if Homebrew::EnvConfig.disable_load_formula?
migrate_gcc_dependents_if_needed
hub = ReporterHub.new
updated_taps = []
Tap.installed.each do |tap|
next if !tap.git? || tap.git_repository.origin_url.nil?
next if (tap.core_tap? || tap.core_cask_tap?) && !Homebrew::EnvConfig.no_install_from_api?
if ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"].present? && tap.core_tap? &&
Settings.read("linuxbrewmigrated") != "true"
ohai "Migrating formulae from linuxbrew-core to homebrew-core"
LINUXBREW_CORE_MIGRATION_LIST.each do |name|
begin
formula = Formula[name]
rescue FormulaUnavailableError
next
end
next unless formula.any_version_installed?
keg = formula.installed_kegs.last
tab = keg.tab
# force a `brew upgrade` from the linuxbrew-core version to the homebrew-core version (even if lower)
tab.source["versions"]["version_scheme"] = -1
tab.write
end
Settings.write "linuxbrewmigrated", true
end
begin
reporter = Reporter.new(tap)
rescue Reporter::ReporterRevisionUnsetError => e
if Homebrew::EnvConfig.developer?
require "utils/backtrace"
onoe "#{e.message}\n#{Utils::Backtrace.clean(e)&.join("\n")}"
end
next
end
if reporter.updated?
updated_taps << tap.name
hub.add(reporter, auto_update: args.auto_update?)
end
end
# If we're installing from the API: we cannot use Git to check for #
# differences in packages so instead use {formula,cask}_names.txt to do so.
# The first time this runs: we won't yet have a base state
# ({formula,cask}_names.before.txt) to compare against so we don't output a
# anything and just copy the files for next time.
unless Homebrew::EnvConfig.no_install_from_api?
api_cache = Homebrew::API::HOMEBREW_CACHE_API
core_tap = CoreTap.instance
cask_tap = CoreCaskTap.instance
[
[:formula, core_tap, core_tap.formula_dir],
[:cask, cask_tap, cask_tap.cask_dir],
].each do |type, tap, dir|
names_txt = api_cache/"#{type}_names.txt"
next unless names_txt.exist?
names_before_txt = api_cache/"#{type}_names.before.txt"
if names_before_txt.exist?
reporter = Reporter.new(
tap,
api_names_txt: names_txt,
api_names_before_txt: names_before_txt,
api_dir_prefix: dir,
)
if reporter.updated?
updated_taps << tap.name
hub.add(reporter, auto_update: args.auto_update?)
end
else
FileUtils.cp names_txt, names_before_txt
end
end
end
unless updated_taps.empty?
auto_update_header
puts "Updated #{Utils.pluralize("tap", updated_taps.count,
include_count: true)} (#{updated_taps.to_sentence})."
updated = true
end
if updated
if hub.empty?
puts no_changes_message unless args.quiet?
else
if ENV.fetch("HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED", false)
opoo "HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED is now the default behaviour, " \
"so you can unset it from your environment."
end
hub.dump(auto_update: args.auto_update?) unless args.quiet?
hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each(&:migrate_cask_rename)
hub.reporters.each { |r| r.migrate_formula_rename(force: args.force?, verbose: args.verbose?) }
CacheStoreDatabase.use(:descriptions) do |db|
DescriptionCacheStore.new(db)
.update_from_report!(hub)
end
CacheStoreDatabase.use(:cask_descriptions) do |db|
CaskDescriptionCacheStore.new(db)
.update_from_report!(hub)
end
end
puts if args.auto_update?
elsif !args.auto_update? && !ENV["HOMEBREW_UPDATE_FAILED"] && !ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"]
puts "Already up-to-date." unless args.quiet?
end
Commands.rebuild_commands_completion_list
link_completions_manpages_and_docs
Tap.installed.each(&:link_completions_and_manpages)
failed_fetch_dirs = ENV["HOMEBREW_MISSING_REMOTE_REF_DIRS"]&.split("\n")
if failed_fetch_dirs.present?
failed_fetch_taps = failed_fetch_dirs.map { |dir| Tap.from_path(dir) }
ofail <<~EOS
Some taps failed to update!
The following taps can not read their remote branches:
#{failed_fetch_taps.join("\n ")}
This is happening because the remote branch was renamed or deleted.
Reset taps to point to the correct remote branches by running `brew tap --repair`
EOS
end
return if new_tag.blank? || new_tag == old_tag || args.quiet?
puts
new_major_version, new_minor_version, new_patch_version = new_tag.split(".").map(&:to_i)
old_major_version, old_minor_version = (old_tag.split(".")[0, 2]).map(&:to_i) if old_tag.present?
if old_tag.blank? || new_major_version > old_major_version || new_minor_version > old_minor_version
puts <<~EOS
The #{new_major_version}.#{new_minor_version}.0 release notes are available on the Homebrew Blog:
#{Formatter.url("https://brew.sh/blog/#{new_major_version}.#{new_minor_version}.0")}
EOS
end
return if new_patch_version.zero?
puts <<~EOS
The #{new_tag} changelog can be found at:
#{Formatter.url("https://github.com/Homebrew/brew/releases/tag/#{new_tag}")}
EOS
end
def no_changes_message
"No changes to formulae or casks."
end
def shorten_revision(revision)
Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
end
def tap_or_untap_core_taps_if_necessary
return if ENV["HOMEBREW_UPDATE_TEST"]
if Homebrew::EnvConfig.no_install_from_api?
return if Homebrew::EnvConfig.automatically_set_no_install_from_api?
core_tap = CoreTap.instance
return if core_tap.installed?
core_tap.ensure_installed!
revision = CoreTap.instance.git_head
ENV["HOMEBREW_UPDATE_BEFORE_HOMEBREW_HOMEBREW_CORE"] = revision
ENV["HOMEBREW_UPDATE_AFTER_HOMEBREW_HOMEBREW_CORE"] = revision
else
return if Homebrew::EnvConfig.developer? || ENV["HOMEBREW_DEV_CMD_RUN"]
return if ENV["HOMEBREW_GITHUB_HOSTED_RUNNER"] || ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"]
return if (HOMEBREW_PREFIX/".homebrewdocker").exist?
tap_output_header_printed = T.let(false, T::Boolean)
[CoreTap.instance, CoreCaskTap.instance].each do |tap|
next unless tap.installed?
if tap.git_branch == "master" &&
(Date.parse(T.must(tap.git_repository.last_commit_date)) <= Date.today.prev_month)
ohai "#{tap.name} is old and unneeded, untapping to save space..."
tap.uninstall
else
unless tap_output_header_printed
puts "Installing from the API is now the default behaviour!"
puts "You can save space and time by running:"
tap_output_header_printed = true
end
puts " brew untap #{tap.name}"
end
end
end
end
def link_completions_manpages_and_docs(repository = HOMEBREW_REPOSITORY)
command = "brew update"
Utils::Link.link_completions(repository, command)
Utils::Link.link_manpages(repository, command)
Utils::Link.link_docs(repository, command)
rescue => e
ofail <<~EOS
Failed to link all completions, docs and manpages:
#{e}
EOS
end
def migrate_gcc_dependents_if_needed
# do nothing
end
def analytics_message
return if Utils::Analytics.messages_displayed?
return if Utils::Analytics.no_message_output?
if Utils::Analytics.disabled? && !Utils::Analytics.influx_message_displayed?
ohai "Homebrew's analytics have entirely moved to our InfluxDB instance in the EU."
puts "We gather less data than before and have destroyed all Google Analytics data:"
puts " #{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}"
puts "Please reconsider re-enabling analytics to help our volunteer maintainers with:"
puts " brew analytics on"
elsif !Utils::Analytics.disabled?
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
# Use the shell's audible bell.
print "\a"
# Use an extra newline and bold to avoid this being missed.
ohai "Homebrew collects anonymous analytics."
puts <<~EOS
#{Tty.bold}Read the analytics documentation (and how to opt-out) here:
#{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}
No analytics have been recorded yet (nor will be during this `brew` run).
EOS
end
# Consider the messages possibly missed if not a TTY.
Utils::Analytics.messages_displayed! if $stdout.tty?
end
def donation_message
return if Settings.read("donationmessage") == "true"
ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:"
puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n\n"
# Consider the message possibly missed if not a TTY.
Settings.write "donationmessage", true if $stdout.tty?
end
def install_from_api_message
return if Settings.read("installfromapimessage") == "true"
no_install_from_api_set = Homebrew::EnvConfig.no_install_from_api? &&
!Homebrew::EnvConfig.automatically_set_no_install_from_api?
return unless no_install_from_api_set
ohai "You have HOMEBREW_NO_INSTALL_FROM_API set"
puts "Homebrew >=4.1.0 is dramatically faster and less error-prone when installing"
puts "from the JSON API. Please consider unsetting HOMEBREW_NO_INSTALL_FROM_API."
puts "This message will only be printed once."
puts "\n\n"
# Consider the message possibly missed if not a TTY.
Settings.write "installfromapimessage", true if $stdout.tty?
end
end
end
end
require "extend/os/cmd/update-report"
class Reporter
class ReporterRevisionUnsetError < RuntimeError
def initialize(var_name)
super "#{var_name} is unset!"
end
end
def initialize(tap, api_names_txt: nil, api_names_before_txt: nil, api_dir_prefix: nil)
@tap = tap
# This is slightly involved/weird but all the #report logic is shared so it's worth it.
if installed_from_api?(api_names_txt, api_names_before_txt, api_dir_prefix)
@api_names_txt = api_names_txt
@api_names_before_txt = api_names_before_txt
@api_dir_prefix = api_dir_prefix
else
initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{tap.repo_var_suffix}"
@initial_revision = ENV[initial_revision_var].to_s
raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty?
current_revision_var = "HOMEBREW_UPDATE_AFTER#{tap.repo_var_suffix}"
@current_revision = ENV[current_revision_var].to_s
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
end
def report(auto_update: false)
return @report if @report
@report = Hash.new { |h, k| h[k] = [] }
return @report unless updated?
diff.each_line do |line|
status, *paths = line.split
src = Pathname.new paths.first
dst = Pathname.new paths.last
next if dst.extname != ".rb"
if paths.any? { |p| tap.cask_file?(p) }
case status
when "A"
# Have a dedicated report array for new casks.
@report[:AC] << tap.formula_file_to_name(src)
when "D"
# Have a dedicated report array for deleted casks.
@report[:DC] << tap.formula_file_to_name(src)
when "M"
# Report updated casks
@report[:MC] << tap.formula_file_to_name(src)
when /^R\d{0,3}/
src_full_name = tap.formula_file_to_name(src)
dst_full_name = tap.formula_file_to_name(dst)
# Don't report formulae that are moved within a tap but not renamed
next if src_full_name == dst_full_name
@report[:DC] << src_full_name
@report[:AC] << dst_full_name
end
end
next unless paths.any? { |p| tap.formula_file?(p) }
case status
when "A", "D"
full_name = tap.formula_file_to_name(src)
name = full_name.split("/").last
new_tap = tap.tap_migrations[name]
@report[status.to_sym] << full_name unless new_tap
when "M"
name = tap.formula_file_to_name(src)
@report[:M] << name
when /^R\d{0,3}/
src_full_name = tap.formula_file_to_name(src)
dst_full_name = tap.formula_file_to_name(dst)
# Don't report formulae that are moved within a tap but not renamed
next if src_full_name == dst_full_name
@report[:D] << src_full_name
@report[:A] << dst_full_name
end
end
renamed_casks = Set.new
@report[:DC].each do |old_full_name|
old_name = old_full_name.split("/").last
new_name = tap.cask_renames[old_name]
next unless new_name
new_full_name = if tap.core_cask_tap?
new_name
else
"#{tap}/#{new_name}"
end
renamed_casks << [old_full_name, new_full_name] if @report[:AC].include?(new_full_name)
end
@report[:AC].each do |new_full_name|
new_name = new_full_name.split("/").last
old_name = tap.cask_renames.key(new_name)
next unless old_name
old_full_name = if tap.core_cask_tap?
old_name
else
"#{tap}/#{old_name}"
end
renamed_casks << [old_full_name, new_full_name]
end
if renamed_casks.any?
@report[:AC] -= renamed_casks.map(&:last)
@report[:DC] -= renamed_casks.map(&:first)
@report[:RC] = renamed_casks.to_a
end
renamed_formulae = Set.new
@report[:D].each do |old_full_name|
old_name = old_full_name.split("/").last
new_name = tap.formula_renames[old_name]
next unless new_name
new_full_name = if tap.core_tap?
new_name
else
"#{tap}/#{new_name}"
end
renamed_formulae << [old_full_name, new_full_name] if @report[:A].include? new_full_name
end
@report[:A].each do |new_full_name|
new_name = new_full_name.split("/").last
old_name = tap.formula_renames.key(new_name)
next unless old_name
old_full_name = if tap.core_tap?
old_name
else
"#{tap}/#{old_name}"
end
renamed_formulae << [old_full_name, new_full_name]
end
if renamed_formulae.any?
@report[:A] -= renamed_formulae.map(&:last)
@report[:D] -= renamed_formulae.map(&:first)
@report[:R] = renamed_formulae.to_a
end
# If any formulae/casks are marked as added and deleted, remove them from
# the report as we've not detected things correctly.
if (added_and_deleted_formulae = (@report[:A] & @report[:D]).presence)
@report[:A] -= added_and_deleted_formulae
@report[:D] -= added_and_deleted_formulae
end
if (added_and_deleted_casks = (@report[:AC] & @report[:DC]).presence)
@report[:AC] -= added_and_deleted_casks
@report[:DC] -= added_and_deleted_casks
end
@report
end
def updated?
if installed_from_api?
diff.present?
else
initial_revision != current_revision
end
end
def migrate_tap_migration
(report[:D] + report[:DC]).each do |full_name|
name = full_name.split("/").last
new_tap_name = tap.tap_migrations[name]
next if new_tap_name.nil? # skip if not in tap_migrations list.
new_tap_user, new_tap_repo, new_tap_new_name = new_tap_name.split("/")
new_name = if new_tap_new_name
new_full_name = new_tap_new_name
new_tap_name = "#{new_tap_user}/#{new_tap_repo}"
new_tap_new_name
else
new_full_name = "#{new_tap_name}/#{name}"
name
end
# This means it is a cask
if report[:DC].include? full_name
next unless (HOMEBREW_PREFIX/"Caskroom"/new_name).exist?
new_tap = Tap.fetch(new_tap_name)
new_tap.ensure_installed!
ohai "#{name} has been moved to Homebrew.", <<~EOS
To uninstall the cask, run:
brew uninstall --cask --force #{name}
EOS
next if (HOMEBREW_CELLAR/new_name.split("/").last).directory?
ohai "Installing #{new_name}..."
system HOMEBREW_BREW_FILE, "install", new_full_name
begin
unless Formulary.factory(new_full_name).keg_only?
system HOMEBREW_BREW_FILE, "link", new_full_name, "--overwrite"
end
rescue Exception => e # rubocop:disable Lint/RescueException
if Homebrew::EnvConfig.developer?
require "utils/backtrace"
onoe "#{e.message}\n#{Utils::Backtrace.clean(e)&.join("\n")}"
end
end
next
end
next unless (dir = HOMEBREW_CELLAR/name).exist? # skip if formula is not installed.
tabs = dir.subdirs.map { |d| Keg.new(d).tab }
next if tabs.first.tap != tap # skip if installed formula is not from this tap.
new_tap = Tap.fetch(new_tap_name)
# For formulae migrated to cask: Auto-install cask or provide install instructions.
if new_tap_name.start_with?("homebrew/cask")
if new_tap.installed? && (HOMEBREW_PREFIX/"Caskroom").directory?
ohai "#{name} has been moved to Homebrew Cask."
ohai "brew unlink #{name}"
system HOMEBREW_BREW_FILE, "unlink", name
ohai "brew cleanup"
system HOMEBREW_BREW_FILE, "cleanup"
ohai "brew install --cask #{new_name}"
system HOMEBREW_BREW_FILE, "install", "--cask", new_name
ohai <<~EOS
#{name} has been moved to Homebrew Cask.
The existing keg has been unlinked.
Please uninstall the formula when convenient by running:
brew uninstall --force #{name}
EOS
else
ohai "#{name} has been moved to Homebrew Cask.", <<~EOS
To uninstall the formula and install the cask, run:
brew uninstall --force #{name}
brew tap #{new_tap_name}
brew install --cask #{new_name}
EOS
end
else
new_tap.ensure_installed!
# update tap for each Tab
tabs.each { |tab| tab.tap = new_tap }
tabs.each(&:write)
end
end
end
def migrate_cask_rename
Cask::Caskroom.casks.each do |cask|
Cask::Migrator.migrate_if_needed(cask)
end
end
def migrate_formula_rename(force:, verbose:)
Formula.installed.each do |formula|
next unless Migrator.needs_migration?(formula)
oldnames_to_migrate = formula.oldnames.select do |oldname|
oldname_rack = HOMEBREW_CELLAR/oldname
next false unless oldname_rack.exist?
if oldname_rack.subdirs.empty?
oldname_rack.rmdir_if_possible
next false
end
true
end
next if oldnames_to_migrate.empty?
Migrator.migrate_if_needed(formula, force:)
end
end
private
attr_reader :tap, :initial_revision, :current_revision, :api_names_txt, :api_names_before_txt, :api_dir_prefix
def installed_from_api?(api_names_txt = @api_names_txt, api_names_before_txt = @api_names_before_txt,
api_dir_prefix = @api_dir_prefix)
!api_names_txt.nil? && !api_names_before_txt.nil? && !api_dir_prefix.nil?
end
def diff
@diff ||= if installed_from_api?
# Hack `git diff` output with regexes to look like `git diff-tree` output.
# Yes, I know this is a bit filthy but it saves duplicating the #report logic.
diff_output = Utils.popen_read("git", "diff", "--no-ext-diff", api_names_before_txt, api_names_txt)
header_regex = /^(---|\+\+\+) /
add_delete_characters = ["+", "-"].freeze
diff_output.lines.filter_map do |line|
next if line.match?(header_regex)
next unless add_delete_characters.include?(line[0])
line.sub(/^\+/, "A #{api_dir_prefix.basename}/")
.sub(/^-/, "D #{api_dir_prefix.basename}/")
.sub(/$/, ".rb")
.chomp
end.join("\n")
else
Utils.popen_read(
"git", "-C", tap.path, "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
"-M85%", initial_revision, current_revision
)
end
end
end
class ReporterHub
attr_reader :reporters
sig { void }
def initialize
@hash = {}
@reporters = []
end
def select_formula_or_cask(key)
@hash.fetch(key, [])
end
def add(reporter, auto_update: false)
@reporters << reporter
report = reporter.report(auto_update:).delete_if { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
end
def empty?
@hash.empty?
end
def dump(auto_update: false)
unless Homebrew::EnvConfig.no_update_report_new?
dump_new_formula_report
dump_new_cask_report
end
dump_deleted_formula_report
dump_deleted_cask_report
outdated_formulae = Formula.installed.select(&:outdated?).map(&:name)
outdated_casks = Cask::Caskroom.casks.select(&:outdated?).map(&:token)
unless auto_update
output_dump_formula_or_cask_report "Outdated Formulae", outdated_formulae
output_dump_formula_or_cask_report "Outdated Casks", outdated_casks
end
return if outdated_formulae.blank? && outdated_casks.blank?
outdated_formulae = outdated_formulae.count
outdated_casks = outdated_casks.count
update_pronoun = if (outdated_formulae + outdated_casks) == 1
"it"
else
"them"
end
msg = ""
if outdated_formulae.positive?
noun = Utils.pluralize("formula", outdated_formulae, plural: "e")
msg += "#{Tty.bold}#{outdated_formulae}#{Tty.reset} outdated #{noun}"
end
if outdated_casks.positive?
msg += " and " if msg.present?
msg += "#{Tty.bold}#{outdated_casks}#{Tty.reset} outdated #{Utils.pluralize("cask", outdated_casks)}"
end
return if msg.blank?
puts
puts "You have #{msg} installed."
# If we're auto-updating, don't need to suggest commands that we're perhaps
# already running.
return if auto_update
puts <<~EOS
You can upgrade #{update_pronoun} with #{Tty.bold}brew upgrade#{Tty.reset}
or list #{update_pronoun} with #{Tty.bold}brew outdated#{Tty.reset}.
EOS
end
private
def dump_new_formula_report
formulae = select_formula_or_cask(:A).sort.reject { |name| installed?(name) }
output_dump_formula_or_cask_report "New Formulae", formulae
end
def dump_new_cask_report
return if Homebrew::SimulateSystem.simulating_or_running_on_linux?
casks = select_formula_or_cask(:AC).sort.filter_map do |name|
name.split("/").last unless cask_installed?(name)
end
output_dump_formula_or_cask_report "New Casks", casks
end
def dump_deleted_formula_report
formulae = select_formula_or_cask(:D).sort.filter_map do |name|
pretty_uninstalled(name) if installed?(name)
end
output_dump_formula_or_cask_report "Deleted Installed Formulae", formulae
end
def dump_deleted_cask_report
return if Homebrew::SimulateSystem.simulating_or_running_on_linux?
casks = select_formula_or_cask(:DC).sort.filter_map do |name|
name = name.split("/").last
pretty_uninstalled(name) if cask_installed?(name)
end
output_dump_formula_or_cask_report "Deleted Installed Casks", casks
end
def output_dump_formula_or_cask_report(title, formulae_or_casks)
return if formulae_or_casks.blank?
ohai title, Formatter.columns(formulae_or_casks.sort)
end
def installed?(formula)
(HOMEBREW_CELLAR/formula.split("/").last).directory?
end
def outdated?(formula)
Formula[formula].outdated?
rescue FormulaUnavailableError
false
end
def cask_installed?(cask)
(Cask::Caskroom.path/cask).directory?
end
def cask_outdated?(cask)
Cask::CaskLoader.load(cask).outdated?
rescue Cask::CaskError
false
end
end