 11d6785bea
			
		
	
	
		11d6785bea
		
			
		
	
	
	
	
		
			
			This is primarily intended to resolve the `uninitialized constant Utils::Backtrace` error in `formula_versions.rb:60` but I expanded it to try to cover all existing usage of `Utils::Backtrace`. I've followed the existing pattern, where `utils/backtrace` is required in the context of where it's used. Many of these cases use `Backtrace` in a conditional manner, so I've tried to ensure that the `require` follows suit.
		
			
				
	
	
		
			873 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			873 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: true
 | |
| # 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
 | |
|     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
 | |
|     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
 |