diff --git a/Library/Homebrew/cask/upgrade.rb b/Library/Homebrew/cask/upgrade.rb index 99a43f74446a4..d1d07878c4f7e 100644 --- a/Library/Homebrew/cask/upgrade.rb +++ b/Library/Homebrew/cask/upgrade.rb @@ -7,6 +7,7 @@ require "deprecate_disable" require "install" require "upgrade" +require "version_change" require "utils/output" require "cask/installer" @@ -32,7 +33,7 @@ def self.greedy_casks greedy: T.nilable(T::Boolean), greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean), - summary_pinned: T.nilable(T::Array[String]), + summary_pinned: T.nilable(T::Array[VersionChange]), summary_disabled: T.nilable(T::Array[String]), ).returns(T::Array[Cask]) } @@ -82,28 +83,29 @@ def self.outdated_casks(casks, args:, force:, quiet:, pinned_casks = outdated_casks.select(&:pinned?) outdated_casks -= pinned_casks pinned_versions = pinned_casks.map do |cask| - "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" + VersionChange.new(name: cask.full_name, old_version: cask.installed_version.to_s, + new_version: cask.version.to_s) end summary_pinned&.concat(pinned_versions) if pinned_casks.any? && ((!quiet && summary_pinned.nil?) || casks.any?) message = "Not upgrading #{pinned_casks.count} pinned #{::Utils.pluralize("package", pinned_casks.count)}:" casks.any? ? ofail(message) : opoo(message) - $stderr.puts Homebrew::Upgrade.format_upgrade_summary(pinned_versions).join("\n") if + $stderr.puts Formatter.version_changes(pinned_versions).join("\n") if !quiet && summary_pinned.nil? end outdated_casks end - sig { params(cask_upgrades: T::Array[String], dry_run: T.nilable(T::Boolean), verb: String).void } + sig { params(cask_upgrades: T::Array[VersionChange], dry_run: T.nilable(T::Boolean), verb: String).void } def self.show_upgrade_summary(cask_upgrades, dry_run: false, verb: "Upgrading") cask_upgrades = cask_upgrades.uniq return if cask_upgrades.empty? verb = "Would upgrade" if dry_run oh1 "#{verb} #{cask_upgrades.count} outdated #{::Utils.pluralize("package", cask_upgrades.count)}:" - puts Homebrew::Upgrade.format_upgrade_summary(cask_upgrades).join("\n") + puts Formatter.version_changes(cask_upgrades).join("\n") end sig { @@ -124,8 +126,8 @@ def self.show_upgrade_summary(cask_upgrades, dry_run: false, verb: "Upgrading") skip_prefetch: T::Boolean, show_upgrade_summary: T::Boolean, download_queue: T.nilable(Homebrew::DownloadQueue), - summary_upgrades: T.nilable(T::Array[String]), - summary_pinned: T.nilable(T::Array[String]), + summary_upgrades: T.nilable(T::Array[VersionChange]), + summary_pinned: T.nilable(T::Array[VersionChange]), summary_deprecated: T.nilable(T::Array[String]), summary_disabled: T.nilable(T::Array[String]), prefetched_errors: T.nilable(T::Array[StandardError]), @@ -272,7 +274,8 @@ def self.upgrade_casks!( return false if upgradable_casks.empty? && !failed cask_upgrades = upgradable_casks.map do |(old_cask, new_cask)| - "#{new_cask.full_name} #{old_cask.version} -> #{new_cask.version}" + VersionChange.new(name: new_cask.full_name, old_version: old_cask.version.to_s, + new_version: new_cask.version.to_s) end summary_upgrades&.concat(cask_upgrades) if dry_run summary_deprecated&.concat(upgradable_casks.filter_map do |(_, new_cask)| diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index bcaa3d6e78dad..8d0bbb1642c6f 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -210,7 +210,7 @@ def run [T::Array[Formula], T::Array[Cask::Cask]], ) ask = !args.no_ask? && !args.dry_run? - cask_upgrade_summary = T.let([], T::Array[String]) + cask_upgrade_summary = T.let([], T::Array[VersionChange]) installed_casks = T.let([], T::Array[Cask::Cask]) new_casks = T.let([], T::Array[Cask::Cask]) diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index b96eea1681c93..d3e0bc86f8ea5 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -26,15 +26,15 @@ class FormulaeUpgradeContext < T::Struct end class FinalUpgradeSummary < T::Struct - prop :version_changes, T::Array[String], default: [] - prop :dependent_version_changes, T::Array[String], default: [] - prop :pinned_formulae, T::Array[String], default: [] - prop :pinned_casks, T::Array[String], default: [] + prop :version_changes, T::Array[VersionChange], default: [] + prop :dependent_version_changes, T::Array[VersionChange], default: [] + prop :pinned_formulae, T::Array[VersionChange], default: [] + prop :pinned_casks, T::Array[VersionChange], default: [] prop :deprecated, T::Array[String], default: [] prop :disabled, T::Array[String], default: [] prop :source_build_formulae, T::Array[String], default: [] - sig { returns(T::Array[String]) } + sig { returns(T::Array[VersionChange]) } def all_version_changes = version_changes | dependent_version_changes end @@ -255,7 +255,7 @@ def run ) end - planned_fetch_names = final_upgrade_summary.all_version_changes.map { |change| change.split.fetch(0) } + planned_fetch_names = final_upgrade_summary.all_version_changes.map(&:name) if Install.ask_prompt_needed?( planned_names: planned_fetch_names.map do |planned_name| formulae.find { |formula| formula.full_specified_name == planned_name }&.full_name || planned_name @@ -436,7 +436,7 @@ def formulae_upgrade_context(formulae, show_upgrade_summary: true, dry_run: args verb = dry_run ? "Would upgrade" : "Upgrading" oh1 "#{verb} #{formulae_to_install.count} outdated #{Utils.pluralize("package", formulae_to_install.count)}:" - puts Upgrade.format_upgrade_summary(formula_upgrade_descriptions(formulae_to_install)).join("\n") if + puts Formatter.version_changes(formula_version_changes(formulae_to_install)).join("\n") if args.no_ask? end @@ -511,8 +511,8 @@ def final_upgrade_summary context: FormulaeUpgradeContext, include_sizes: T::Boolean, formulae_installer: T.nilable(T::Array[FormulaInstaller]), - version_changes: T.nilable(T::Array[String]), - dependent_version_changes: T.nilable(T::Array[String]), + version_changes: T.nilable(T::Array[VersionChange]), + dependent_version_changes: T.nilable(T::Array[VersionChange]), ).void } def record_formula_upgrade_summary(context, include_sizes: false, formulae_installer: nil, version_changes: nil, @@ -521,13 +521,12 @@ def record_formula_upgrade_summary(context, include_sizes: false, formulae_insta formulae_installer ||= context.formulae_installer upgrade_formulae = formulae_installer.map(&:formula) dependent_formulae = context.dependants.upgradeable - summary.version_changes.concat(version_changes || formula_upgrade_descriptions(upgrade_formulae, - include_sizes:)) + summary.version_changes.concat(version_changes || formula_version_changes(upgrade_formulae, include_sizes:)) summary.dependent_version_changes.concat( - dependent_version_changes || formula_upgrade_descriptions(dependent_formulae, include_sizes:), + dependent_version_changes || formula_version_changes(dependent_formulae, include_sizes:), ) summary.pinned_formulae.concat((context.pinned_formulae + context.dependants.pinned).map do |formula| - Upgrade.formula_upgrade_description(formula) + Upgrade.formula_version_change(formula) end) formulae = context.formulae_to_install + context.pinned_formulae + @@ -566,7 +565,7 @@ def show_final_upgrade_summary(dry_run: args.dry_run?, show_upgrade_summary: arg "#{dry_run ? "Would upgrade" : "Upgraded"} #{version_change_count} " \ "#{"requested " if named}outdated " \ "#{Utils.pluralize("package", version_change_count)}", - Upgrade.format_upgrade_summary(version_changes), + Formatter.version_changes(version_changes), ) end if show_upgrade_summary && named && summary.dependent_version_changes.present? @@ -574,21 +573,21 @@ def show_final_upgrade_summary(dry_run: args.dry_run?, show_upgrade_summary: arg show_final_upgrade_summary_section( "#{dry_run ? "Would upgrade" : "Upgraded"} #{dependent_count} " \ "#{Utils.pluralize("dependent", dependent_count)}", - Upgrade.format_upgrade_summary(summary.dependent_version_changes), + Formatter.version_changes(summary.dependent_version_changes), ) end if summary.pinned_formulae.present? pinned_count = summary.pinned_formulae.uniq.count show_final_upgrade_summary_section( "#{pinned_count} Pinned #{Utils.pluralize("formula", pinned_count)}", - Upgrade.format_upgrade_summary(summary.pinned_formulae.uniq), + Formatter.version_changes(summary.pinned_formulae.uniq), ) end if summary.pinned_casks.present? pinned_count = summary.pinned_casks.uniq.count show_final_upgrade_summary_section( "#{pinned_count} Pinned #{Utils.pluralize("cask", pinned_count)}", - Upgrade.format_upgrade_summary(summary.pinned_casks.uniq), + Formatter.version_changes(summary.pinned_casks.uniq), ) end deprecate_disable_summary = summary.deprecated.map { |item| "#{item} (deprecated)" } + @@ -613,23 +612,16 @@ def show_final_upgrade_summary(dry_run: args.dry_run?, show_upgrade_summary: arg end end - sig { params(formulae: T::Array[Formula], include_sizes: T::Boolean).returns(T::Array[String]) } - def formula_upgrade_descriptions(formulae, include_sizes: false) + sig { params(formulae: T::Array[Formula], include_sizes: T::Boolean).returns(T::Array[VersionChange]) } + def formula_version_changes(formulae, include_sizes: false) formulae.map do |formula| + size = formula_upgrade_size(formula) if include_sizes if formula.optlinked? - old_keg = Keg.new(formula.opt_prefix) - old_version = old_keg.version + old_version = Keg.new(formula.opt_prefix).version new_version = formula_upgrade_display_version(formula, old_version) - if include_sizes - "#{formula.full_specified_name} #{old_version} -> " \ - "#{new_version}#{formula_upgrade_size(formula)}" - else - "#{formula.full_specified_name} #{old_version} -> #{new_version}" - end - elsif include_sizes - "#{formula.full_specified_name} #{formula.pkg_version}#{formula_upgrade_size(formula)}" + VersionChange.new(name: formula.full_specified_name, old_version: old_version.to_s, new_version:, size:) else - "#{formula.full_specified_name} #{formula.pkg_version}" + VersionChange.new(name: formula.full_specified_name, new_version: formula.pkg_version.to_s, size:) end end end @@ -642,7 +634,7 @@ def formula_upgrade_descriptions(formulae, include_sizes: false) dry_run: T::Boolean, download_queue: T.nilable(Homebrew::DownloadQueue), prefetch_names: T.nilable(T::Array[String]), - prefetch_upgrades: T.nilable(T::Array[String]), + prefetch_upgrades: T.nilable(T::Array[VersionChange]), show_upgrade_summary: T::Boolean, ).returns(T::Boolean) } @@ -693,7 +685,7 @@ def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: f .uniq { |fi| fi.formula.full_name }, download_queue: prefetch_download_queue) prefetch_names&.replace(valid_formula_installers.map { |fi| fi.formula.name }) - prefetch_upgrades&.replace(formula_upgrade_descriptions(valid_formula_installers.map(&:formula))) + prefetch_upgrades&.replace(formula_version_changes(valid_formula_installers.map(&:formula))) valid_dependent_formulae_installer = valid_formula_installers & dependent_formulae_installer @prefetched_formulae_upgrade_context = FormulaeUpgradeContext.new( formulae_to_install: context.formulae_to_install, @@ -709,11 +701,10 @@ def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: f return true end - planned_formula_version_changes = formula_upgrade_descriptions(context.formulae_installer.map(&:formula), - include_sizes: dry_run) + planned_formula_version_changes = formula_version_changes(context.formulae_installer.map(&:formula), + include_sizes: dry_run) dependent_formulae = context.dependants.upgradeable.dup - planned_dependent_version_changes = formula_upgrade_descriptions(dependent_formulae, - include_sizes: dry_run) + planned_dependent_version_changes = formula_version_changes(dependent_formulae, include_sizes: dry_run) if dry_run record_formula_upgrade_summary(context, version_changes: planned_formula_version_changes, @@ -792,7 +783,7 @@ def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: f sig { params(casks: T::Array[Cask::Cask], download_queue: Homebrew::DownloadQueue, prefetch_names: T.nilable(T::Array[String]), - prefetch_upgrades: T.nilable(T::Array[String]), + prefetch_upgrades: T.nilable(T::Array[VersionChange]), prefetch_casks: T.nilable(T::Array[Cask::Cask]), prefetch_installers: T.nilable(T::Array[Cask::Installer]), prefetch_errors: T.nilable(T::Array[StandardError])) @@ -858,7 +849,10 @@ def prefetch_outdated_casks!(casks, download_queue:, prefetch_names: nil, prefetch_casks&.replace(valid_casks) prefetch_names&.replace(valid_casks.map(&:full_name)) prefetch_upgrades&.replace( - valid_casks.map { |cask| "#{cask.full_name} #{cask.installed_version} -> #{cask.version}" }, + valid_casks.map do |cask| + VersionChange.new(name: cask.full_name, old_version: cask.installed_version.to_s, + new_version: cask.version.to_s) + end, ) true rescue => e @@ -986,17 +980,17 @@ def formula_upgrade_display_version(formula, old_version) latest_head_version.to_s end - sig { params(formula: Formula).returns(String) } + sig { params(formula: Formula).returns(T.nilable(String)) } def formula_upgrade_size(formula) - return "" if args.build_from_source_formulae.include?(formula.name) + return if args.build_from_source_formulae.include?(formula.name) bottle = formula.bottle - return "" unless bottle + return unless bottle bottle.fetch_tab(quiet: !args.debug?) - return "" unless (download_size = bottle.bottle_size) + return unless (download_size = bottle.bottle_size) - " (#{Formatter.disk_usage_readable(download_size.to_i)})" + Formatter.disk_usage_readable(download_size.to_i) end end end diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index 933af358bfb52..1cf1b4548eb9e 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -23,7 +23,7 @@ module Homebrew module Install extend Utils::Output::Mixin - DependencySummary = T.type_alias { T::Hash[Symbol, T::Array[String]] } + DependencySummary = T.type_alias { T::Hash[Symbol, T::Array[VersionChange]] } class << self sig { params(all_fatal: T::Boolean).void } @@ -379,11 +379,11 @@ def install_formulae( groups.keys.sort.each do |action| group = groups.fetch(action) ohai "Would #{action} #{Utils.pluralize("formula", group.count, include_count: true)}:" - puts Upgrade.format_upgrade_summary(group.map do |formula| + puts Formatter.version_changes(group.map do |formula| if action == "upgrade" - Upgrade.formula_upgrade_description(formula) + Upgrade.formula_version_change(formula) else - "#{formula.full_specified_name} #{formula.pkg_version}" + VersionChange.new(name: formula.full_specified_name, new_version: formula.pkg_version.to_s) end end) end @@ -391,7 +391,9 @@ def install_formulae( formula_installers.each do |fi| next if fi.ignore_deps? - print_dry_run_dependencies(fi.formula, fi.compute_dependencies, &:name) + print_dry_run_dependencies(fi.formula, fi.compute_dependencies) do |dependency| + Upgrade.formula_version_change(dependency) + end end return [] end @@ -423,7 +425,7 @@ def install_formulae( dependencies: T::Array[Dependency], skip_formula_names: T::Array[String], dependency_summary: T.nilable(DependencySummary), - _block: T.proc.params(arg0: Formula).returns(String), + _block: T.proc.params(arg0: Formula).returns(VersionChange), ).void } def print_dry_run_dependencies(formula, dependencies, skip_formula_names: [], dependency_summary: nil, &_block) @@ -453,7 +455,7 @@ def print_dry_run_dependency_summary(summary, formula: nil) ohai "Would #{verb} #{Utils.pluralize("dependency", group.count, include_count: true)}" \ "#{" for #{formula.name}" if formula}:" - puts Upgrade.format_upgrade_summary(group) + puts Formatter.version_changes(group) end end diff --git a/Library/Homebrew/sorbet/rbi/shims/rspec.rbi b/Library/Homebrew/sorbet/rbi/shims/rspec.rbi index 19fb70f658b75..a70add58b181c 100644 --- a/Library/Homebrew/sorbet/rbi/shims/rspec.rbi +++ b/Library/Homebrew/sorbet/rbi/shims/rspec.rbi @@ -12,6 +12,7 @@ class RSpec::Core::ExampleGroup include Test::Helper::Cask include Test::Helper::Fixtures include Test::Helper::Formula + include Test::Helper::VersionChange include Test::Helper::IntegrationTest include Test::Helper::MkTmpDir include Test::Helper::Subcommand diff --git a/Library/Homebrew/test/cask/upgrade_spec.rb b/Library/Homebrew/test/cask/upgrade_spec.rb index cc81ec7a43673..f586389834f5d 100644 --- a/Library/Homebrew/test/cask/upgrade_spec.rb +++ b/Library/Homebrew/test/cask/upgrade_spec.rb @@ -112,10 +112,10 @@ def write_info_plist(path, short_version:, bundle_version:) expect(described_class).to receive(:show_upgrade_summary) do |cask_upgrades, dry_run:| expect(dry_run).to be(true) expect(cask_upgrades).to include( - "local-caffeine 1.2.2 -> 1.2.3", - "local-transmission-zip 2.60 -> 2.61", - "auto-updates 2.57 -> 2.61", - "renamed-app 1.0.0 -> 2.0.0", + version_change("local-caffeine", "1.2.2", "1.2.3"), + version_change("local-transmission-zip", "2.60", "2.61"), + version_change("auto-updates", "2.57", "2.61"), + version_change("renamed-app", "1.0.0", "2.0.0"), ) expect(cask_upgrades.grep(/version-latest/)).to be_empty end @@ -156,9 +156,9 @@ def write_info_plist(path, short_version:, bundle_version:) expect(described_class).to receive(:show_upgrade_summary) do |cask_upgrades, dry_run:| expect(dry_run).to be(true) expect(cask_upgrades).to include( - "local-caffeine 1.2.2 -> 1.2.3", - "local-transmission-zip 2.60 -> 2.61", - "renamed-app 1.0.0 -> 2.0.0", + version_change("local-caffeine", "1.2.2", "1.2.3"), + version_change("local-transmission-zip", "2.60", "2.61"), + version_change("renamed-app", "1.0.0", "2.0.0"), ) expect(cask_upgrades.grep(/auto-updates/)).to be_empty end @@ -200,9 +200,9 @@ def write_info_plist(path, short_version:, bundle_version:) expect(described_class).to receive(:show_upgrade_summary) do |cask_upgrades, dry_run:| expect(dry_run).to be(true) expect(cask_upgrades).to include( - "local-caffeine 1.2.2 -> 1.2.3", - "local-transmission-zip 2.60 -> 2.61", - "renamed-app 1.0.0 -> 2.0.0", + version_change("local-caffeine", "1.2.2", "1.2.3"), + version_change("local-transmission-zip", "2.60", "2.61"), + version_change("renamed-app", "1.0.0", "2.0.0"), ) expect(cask_upgrades.grep(/auto-updates/)).to be_empty end @@ -224,7 +224,7 @@ def write_info_plist(path, short_version:, bundle_version:) args:, ) - expect(summary_upgrades).to include("local-caffeine 1.2.2 -> 1.2.3") + expect(summary_upgrades).to include(version_change("local-caffeine", "1.2.2", "1.2.3")) expect(summary_deprecated).to include("local-caffeine") end @@ -269,15 +269,15 @@ def write_info_plist(path, short_version:, bundle_version:) expect(described_class).to receive(:show_upgrade_summary) do |cask_upgrades, dry_run:| expect(dry_run).to be(true) expect(cask_upgrades).to include( - "local-transmission-zip 2.60 -> 2.61", - "auto-updates 2.57 -> 2.61", - "renamed-app 1.0.0 -> 2.0.0", + version_change("local-transmission-zip", "2.60", "2.61"), + version_change("auto-updates", "2.57", "2.61"), + version_change("renamed-app", "1.0.0", "2.0.0"), ) expect(cask_upgrades.grep(/local-caffeine/)).to be_empty end described_class.upgrade_casks!(dry_run: true, quiet: true, summary_pinned:, args:) - expect(summary_pinned).to include("local-caffeine 1.2.2 -> 1.2.3") + expect(summary_pinned).to include(version_change("local-caffeine", "1.2.2", "1.2.3")) ensure local_caffeine.unpin end @@ -303,7 +303,7 @@ def write_info_plist(path, short_version:, bundle_version:) it "would update only the Casks specified in the command line" do expect(described_class).not_to receive(:upgrade_cask) expect(described_class).to receive(:show_upgrade_summary) - .with(["local-caffeine 1.2.2 -> 1.2.3"], dry_run: true) + .with([version_change("local-caffeine", "1.2.2", "1.2.3")], dry_run: true) expect(local_caffeine).to be_installed expect(local_caffeine_path).to be_a_directory @@ -327,7 +327,8 @@ def write_info_plist(path, short_version:, bundle_version:) it 'would update "auto_updates" and "latest" Casks when their tokens are provided in the command line' do expect(described_class).not_to receive(:upgrade_cask) expect(described_class).to receive(:show_upgrade_summary) - .with(["local-caffeine 1.2.2 -> 1.2.3", "auto-updates 2.57 -> 2.61"], dry_run: true) + .with([version_change("local-caffeine", "1.2.2", "1.2.3"), + version_change("auto-updates", "2.57", "2.61")], dry_run: true) expect(local_caffeine).to be_installed expect(local_caffeine_path).to be_a_directory @@ -411,7 +412,7 @@ def write_info_plist(path, short_version:, bundle_version:) it 'would update outdated Casks with "auto_updates true"' do expect(described_class).not_to receive(:upgrade_cask) expect(described_class).to receive(:show_upgrade_summary) - .with(["auto-updates 2.57 -> 2.61"], dry_run: true) + .with([version_change("auto-updates", "2.57", "2.61")], dry_run: true) expect(auto_updates).to be_installed expect(auto_updates_path).to be_a_directory @@ -427,7 +428,7 @@ def write_info_plist(path, short_version:, bundle_version:) it 'would update outdated Casks with "version latest"' do expect(described_class).not_to receive(:upgrade_cask) expect(described_class).to receive(:show_upgrade_summary) - .with(["version-latest latest -> latest"], dry_run: true) + .with([version_change("version-latest", "latest", "latest")], dry_run: true) expect(version_latest).to be_installed expect(version_latest_paths).to all be_a_directory @@ -1008,7 +1009,7 @@ def write_info_plist(path, short_version:, bundle_version:) end.to output(/bad-checksum: failed.*bad-checksum2: failed/m).to_stderr expect(upgraded_tokens).to contain_exactly("bad-checksum", "bad-checksum2", "local-transmission-zip") - expect(summary_upgrades).to contain_exactly("local-transmission-zip 2.60 -> 2.61") + expect(summary_upgrades).to contain_exactly(version_change("local-transmission-zip", "2.60", "2.61")) expect(bad_checksum).to be_installed expect(bad_checksum_path).to be_a_directory @@ -1064,7 +1065,7 @@ def write_info_plist(path, short_version:, bundle_version:) end.to output(/local-caffeine: This cask does not run on macOS versions older than Tahoe\./).to_stderr expect(upgraded_tokens).to eq(["local-transmission-zip"]) - expect(summary_upgrades).to eq(["local-transmission-zip 2.60 -> 2.61"]) + expect(summary_upgrades).to eq([version_change("local-transmission-zip", "2.60", "2.61")]) end it "reports prefetched requirement errors alongside compatible casks" do @@ -1090,7 +1091,7 @@ def write_info_plist(path, short_version:, bundle_version:) end.to output(/#{Regexp.escape(cask_error.message)}/).to_stderr expect(upgraded_tokens).to eq(["local-transmission-zip"]) - expect(summary_upgrades).to eq(["local-transmission-zip 2.60 -> 2.61"]) + expect(summary_upgrades).to eq([version_change("local-transmission-zip", "2.60", "2.61")]) end end end diff --git a/Library/Homebrew/test/cmd/install_spec.rb b/Library/Homebrew/test/cmd/install_spec.rb index deed725873ba5..0732de0f69850 100644 --- a/Library/Homebrew/test/cmd/install_spec.rb +++ b/Library/Homebrew/test/cmd/install_spec.rb @@ -129,7 +129,7 @@ ==> Would upgrade 1 formula: changed 2.0 ==> Would install 1 dependency for changed: - dependency + dependency 1.0 EOS end @@ -156,7 +156,7 @@ ==> Would install 1 formula: changed 2.0 ==> Would upgrade 1 dependency for changed: - dependency + dependency 1.0 EOS end @@ -728,7 +728,7 @@ def install expect(kwargs[:show_upgrade_summary]).to be(false) package_count.times { |index| Homebrew.messages.package_installed("package-#{index}", 0.0) } - kwargs[:summary_upgrades]&.push("codex 0.117.0 -> 0.118.0") if package_count.positive? + kwargs[:summary_upgrades]&.push(version_change("codex", "0.117.0", "0.118.0")) if package_count.positive? true end diff --git a/Library/Homebrew/test/cmd/upgrade_spec.rb b/Library/Homebrew/test/cmd/upgrade_spec.rb index e9ae68f7614c3..0d90a2be060bc 100644 --- a/Library/Homebrew/test/cmd/upgrade_spec.rb +++ b/Library/Homebrew/test/cmd/upgrade_spec.rb @@ -237,8 +237,8 @@ def stub_formula_upgrade_installers install_head_formula_version "head-formula", "1234567" cmd = described_class.new(["--yes", "--formula", "head-formula"]) - expect(cmd.formula_upgrade_descriptions(cmd.args.named.to_resolved_formulae)) - .to eq(["head-formula HEAD-1234567 -> latest HEAD"]) + expect(cmd.formula_version_changes(cmd.args.named.to_resolved_formulae)) + .to eq([version_change("head-formula", "HEAD-1234567", "latest HEAD")]) end it "describes fetched HEAD formula upgrades with the resolved commit", :no_api do @@ -247,8 +247,8 @@ def stub_formula_upgrade_installers .and_return(PkgVersion.parse("HEAD-7654321")) cmd = described_class.new(["--yes", "--fetch-HEAD", "--formula", "head-formula"]) - expect(cmd.formula_upgrade_descriptions(cmd.args.named.to_resolved_formulae)) - .to eq(["head-formula HEAD-1234567 -> HEAD-7654321"]) + expect(cmd.formula_version_changes(cmd.args.named.to_resolved_formulae)) + .to eq([version_change("head-formula", "HEAD-1234567", "HEAD-7654321")]) end it "skips fetched HEAD formula upgrades when the resolved commit is unchanged", :no_api do @@ -374,13 +374,13 @@ def stub_formula_upgrade_installers it "does not repeat unchanged summary sections after a short upgrade" do cmd = described_class.new([]) - cmd.final_upgrade_summary.pinned_formulae << "pinnedball 1.0 -> 2.0" + cmd.final_upgrade_summary.pinned_formulae << version_change("pinnedball", "1.0", "2.0") cmd.final_upgrade_summary.deprecated << "pinnedball" expect do cmd.show_final_upgrade_summary(dry_run: true) Homebrew.messages.package_installed("testball", 0.0) - cmd.final_upgrade_summary.version_changes << "testball 0.1 -> 0.2" + cmd.final_upgrade_summary.version_changes << version_change("testball", "0.1", "0.2") cmd.show_final_upgrade_summary end.to output(<<~EOS).to_stdout ==> 1 Pinned formula @@ -392,8 +392,8 @@ def stub_formula_upgrade_installers it "repeats unchanged summary sections after two package changes" do cmd = described_class.new([]) - cmd.final_upgrade_summary.pinned_formulae << "pinnedball 1.0 -> 2.0" - cmd.final_upgrade_summary.pinned_casks << "pinned-cask 2.0 -> 3.0" + cmd.final_upgrade_summary.pinned_formulae << version_change("pinnedball", "1.0", "2.0") + cmd.final_upgrade_summary.pinned_casks << version_change("pinned-cask", "2.0", "3.0") cmd.final_upgrade_summary.deprecated << "pinnedball" expect do @@ -415,7 +415,8 @@ def stub_formula_upgrade_installers test_each([0, 1]) do |package_count| it "omits upgrade summaries for #{package_count} completed package changes" do cmd = described_class.new([]) - cmd.final_upgrade_summary.version_changes.push("testball 0.1 -> 0.2", "secondball 1.0 -> 2.0") + cmd.final_upgrade_summary.version_changes.push(version_change("testball", "0.1", "0.2"), + version_change("secondball", "1.0", "2.0")) expect do cmd.show_final_upgrade_summary(dry_run: true) @@ -571,7 +572,7 @@ def stub_formula_upgrade_installers expect(cmd).to receive(:upgrade_outdated_formulae!) .with([], dry_run: true, show_upgrade_summary: false) .ordered do - cmd.final_upgrade_summary.version_changes << "testball 0.1 -> 0.2" + cmd.final_upgrade_summary.version_changes << version_change("testball", "0.1", "0.2") true end expect(cmd).to receive(:upgrade_outdated_casks!) @@ -697,7 +698,7 @@ def stub_formula_upgrade_installers expect(cmd).to receive(:upgrade_outdated_formulae!) .with([formula], dry_run: true, show_upgrade_summary: false) .ordered do - cmd.final_upgrade_summary.version_changes << "testball 0.1 -> 0.2" + cmd.final_upgrade_summary.version_changes << version_change("testball", "0.1", "0.2") true end allow(cmd).to receive(:show_final_upgrade_summary).and_call_original @@ -749,8 +750,8 @@ def stub_formula_upgrade_installers allow(formula).to receive_messages(optlinked?: true, opt_prefix: HOMEBREW_PREFIX/"opt/testball", bottle:) allow(Keg).to receive(:new).with(HOMEBREW_PREFIX/"opt/testball").and_return(keg) - expect(cmd.formula_upgrade_descriptions([formula], include_sizes: true)) - .to eq(["testball 0.1 -> 0.2 (500B)"]) + expect(cmd.formula_version_changes([formula], include_sizes: true)) + .to eq([version_change("testball", "0.1", "0.2", size: "500B")]) end it "omits formula download sizes in dry-run source build upgrade summaries" do @@ -767,8 +768,8 @@ def stub_formula_upgrade_installers allow(Keg).to receive(:new).with(HOMEBREW_PREFIX/"opt/testball").and_return(keg) expect(bottle).not_to receive(:fetch_tab) - expect(cmd.formula_upgrade_descriptions([formula], include_sizes: true)) - .to eq(["testball 0.1 -> 0.2"]) + expect(cmd.formula_version_changes([formula], include_sizes: true)) + .to eq([version_change("testball", "0.1", "0.2")]) end it "prints dry-run cleanup output from one formula cleanup run" do @@ -900,15 +901,15 @@ def stub_formula_upgrade_installers it "does not print aggregate package sizes" do cmd = described_class.new(["--dry-run"]) summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new( - version_changes: ["testball 0.1 -> 0.2 (500B)"], - dependent_version_changes: ["codex 1.0 -> 2.0"], + version_changes: [version_change("testball", "0.1", "0.2", size: "500B")], + dependent_version_changes: [version_change("codex", "1.0", "2.0")], ) allow(cmd).to receive(:final_upgrade_summary).and_return(summary) expect { cmd.show_final_upgrade_summary }.to output(<<~EOS).to_stdout ==> Would upgrade 2 outdated packages - testball 0.1 -> 0.2 (500B) + testball 0.1 -> 0.2 (500B) codex 1.0 -> 2.0 EOS end @@ -916,8 +917,9 @@ def stub_formula_upgrade_installers it "separates requested upgrades from dependent upgrades" do cmd = described_class.new(["--dry-run", "z3"]) summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new( - version_changes: ["z3 4.16.0 -> 5.1.0"], - dependent_version_changes: ["llvm 22.1.8 -> 22.1.8_2", "rust 1.97.1 -> 1.98.0"], + version_changes: [version_change("z3", "4.16.0", "5.1.0")], + dependent_version_changes: [version_change("llvm", "22.1.8", "22.1.8_2"), + version_change("rust", "1.97.1", "1.98.0")], ) allow(cmd).to receive(:final_upgrade_summary).and_return(summary) @@ -934,31 +936,31 @@ def stub_formula_upgrade_installers it "deduplicates combined upgrade summaries while preserving package order" do summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new( version_changes: [ - "openssl@3 3.6.4 -> 3.6.5", - "openssl@4 4.0.2 -> 4.0.3", - "openssl@3 3.6.4 -> 3.6.5", + version_change("openssl@3", "3.6.4", "3.6.5"), + version_change("openssl@4", "4.0.2", "4.0.3"), + version_change("openssl@3", "3.6.4", "3.6.5"), ], dependent_version_changes: [ - "deno 2.7.10 -> 2.7.11", - "openssl@3 3.6.4 -> 3.6.5", - "deno 2.7.10 -> 2.7.11", - "openssl@4 4.0.2 -> 4.0.3", + version_change("deno", "2.7.10", "2.7.11"), + version_change("openssl@3", "3.6.4", "3.6.5"), + version_change("deno", "2.7.10", "2.7.11"), + version_change("openssl@4", "4.0.2", "4.0.3"), ], ) expect(summary.all_version_changes).to eq([ - "openssl@3 3.6.4 -> 3.6.5", - "openssl@4 4.0.2 -> 4.0.3", - "deno 2.7.10 -> 2.7.11", + version_change("openssl@3", "3.6.4", "3.6.5"), + version_change("openssl@4", "4.0.2", "4.0.3"), + version_change("deno", "2.7.10", "2.7.11"), ]) end it "deduplicates the execution upgrade summary" do expect do Cask::Upgrade.show_upgrade_summary([ - "testball 0.1 -> 0.2", - "testball 0.1 -> 0.2", - "codex 1.0 -> 2.0", + version_change("testball", "0.1", "0.2"), + version_change("testball", "0.1", "0.2"), + version_change("codex", "1.0", "2.0"), ]) end.to output(<<~EOS).to_stdout ==> Upgrading 2 outdated packages: @@ -1007,7 +1009,7 @@ def stub_formula_upgrade_installers if prefetch_only expect(show_upgrade_summary).to be(false) prefetch_names&.replace(["deno"]) - prefetch_upgrades&.replace(["deno 2.7.10 -> 2.7.11"]) + prefetch_upgrades&.replace([version_change("deno", "2.7.10", "2.7.11")]) end true @@ -1052,7 +1054,7 @@ def stub_formula_upgrade_installers expect(formulae).to eq([formula]) if prefetch_only prefetch_names&.replace(["testball"]) - prefetch_upgrades&.replace(["testball 0.1 -> 0.2"]) + prefetch_upgrades&.replace([version_change("testball", "0.1", "0.2")]) end true end.twice @@ -1083,7 +1085,7 @@ def stub_formula_upgrade_installers prefetch_casks:, prefetch_installers:, **| expect(casks).to eq([cask]) prefetch_names.replace(["codex"]) - prefetch_upgrades.replace(["codex 1.0 -> 2.0"]) + prefetch_upgrades.replace([version_change("codex", "1.0", "2.0")]) prefetch_casks.replace([cask]) prefetch_installers.replace([installer]) true @@ -1173,10 +1175,10 @@ def stub_formula_upgrade_installers use_prefetched: false, prefetch_names: nil, prefetch_upgrades: nil, **| if dry_run - cmd.final_upgrade_summary.version_changes << "deno 2.7.10 -> 2.7.11" + cmd.final_upgrade_summary.version_changes << version_change("deno", "2.7.10", "2.7.11") elsif prefetch_only prefetch_names&.replace(["deno"]) - prefetch_upgrades&.replace(["deno 2.7.10 -> 2.7.11"]) + prefetch_upgrades&.replace([version_change("deno", "2.7.10", "2.7.11")]) else expect(use_prefetched).to be(true) end @@ -1187,7 +1189,7 @@ def stub_formula_upgrade_installers allow(Cask::Installer).to receive(:new).and_return(installer) allow(Cask::Upgrade).to receive(:upgrade_casks!) do |*_, **kwargs| if kwargs[:dry_run] - kwargs[:summary_upgrades] << "codex 0.117.0 -> 0.118.0" + kwargs[:summary_upgrades] << version_change("codex", "0.117.0", "0.118.0") else expect(kwargs[:skip_prefetch]).to be(true) end @@ -1217,13 +1219,13 @@ def stub_formula_upgrade_installers prefetch_names: nil, **| if dry_run cmd.final_upgrade_summary.version_changes.push( - "deno 2.7.10 -> 2.7.11", - "openssl@3 3.6.4 -> 3.6.5", - "openssl@4 4.0.2 -> 4.0.3", + version_change("deno", "2.7.10", "2.7.11"), + version_change("openssl@3", "3.6.4", "3.6.5"), + version_change("openssl@4", "4.0.2", "4.0.3"), ) cmd.final_upgrade_summary.dependent_version_changes.push( - "openssl@3 3.6.4 -> 3.6.5", - "openssl@4 4.0.2 -> 4.0.3", + version_change("openssl@3", "3.6.4", "3.6.5"), + version_change("openssl@4", "4.0.2", "4.0.3"), ) elsif prefetch_only sequence << "prefetch" @@ -1257,7 +1259,7 @@ def stub_formula_upgrade_installers expect(Homebrew::DownloadQueue).to receive(:new).and_return(download_queue) expect(cmd).to receive(:prefetch_outdated_casks!) do |_, prefetch_upgrades:, prefetch_casks:, prefetch_errors:, **| - prefetch_upgrades.replace(["codex 0.117.0 -> 0.118.0"]) + prefetch_upgrades.replace([version_change("codex", "0.117.0", "0.118.0")]) prefetch_casks.replace([cask]) prefetch_errors << cask_error true @@ -1327,7 +1329,7 @@ def stub_formula_upgrade_installers ), ).to be(true) expect(prefetch_names).to eq(["codex"]) - expect(prefetch_upgrades).to eq(["codex 0.117.0 -> 0.118.0"]) + expect(prefetch_upgrades).to eq([version_change("codex", "0.117.0", "0.118.0")]) expect(prefetch_casks).to eq([compatible_cask]) expect(prefetch_errors.map(&:to_s)).to eq(["bad-cask: This cask requires Linux."]) end @@ -1441,7 +1443,7 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) **| if prefetch_only prefetch_names&.replace(["deno"]) - prefetch_upgrades&.replace(["deno 2.7.10 -> 2.7.11"]) + prefetch_upgrades&.replace([version_change("deno", "2.7.10", "2.7.11")]) elsif !dry_run upgraded[:use_prefetched] = use_prefetched end @@ -1453,7 +1455,7 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) allow(Cask::Upgrade).to receive(:upgrade_casks!) do |*_, **kwargs| if kwargs[:dry_run] # Plan an upgrade in the `--ask` preview so the shared prefetch runs. - kwargs[:summary_upgrades]&.push("codex 0.117.0 -> 0.118.0") + kwargs[:summary_upgrades]&.push(version_change("codex", "0.117.0", "0.118.0")) else upgraded[:skip_prefetch] = kwargs[:skip_prefetch] end @@ -1500,9 +1502,9 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) it "prints a narrow final upgrade summary" do cmd = described_class.new([]) summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new( - version_changes: ["testball 0.1 -> 0.2"], - pinned_formulae: ["pinnedball 1.0"], - pinned_casks: ["pinned-cask 2.0"], + version_changes: [version_change("testball", "0.1", "0.2")], + pinned_formulae: [version_change("pinnedball", "1.0")], + pinned_casks: [version_change("pinned-cask", "2.0")], deprecated: ["oldball"], disabled: ["disabledball"], source_build_formulae: ["sourceball"], @@ -1569,8 +1571,8 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) cmd.record_formula_upgrade_summary(context) summary = cmd.final_upgrade_summary - expect(summary.version_changes).to include("testball 0.1 -> 0.2") - expect(summary.pinned_formulae).to include("pinnedball 1.0") + expect(summary.version_changes).to include(version_change("testball", "0.1", "0.2")) + expect(summary.pinned_formulae).to include(version_change("pinnedball", "1.0")) expect(summary.deprecated).to include("oldball") expect(summary.disabled).to include("disabledball") expect(summary.source_build_formulae).to include("sourceball") @@ -1604,7 +1606,7 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) cmd.upgrade_outdated_formulae!([]) - expect(cmd.final_upgrade_summary.version_changes).to include("testball 0.1 -> 0.2") + expect(cmd.final_upgrade_summary.version_changes).to include(version_change("testball", "0.1", "0.2")) end it "omits failed formula version changes from the final summary" do @@ -1640,7 +1642,7 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) cmd.upgrade_outdated_formulae!([]) expect(cmd.final_upgrade_summary).to have_attributes( - version_changes: contain_exactly("testball 0.1 -> 0.2"), + version_changes: contain_exactly(version_change("testball", "0.1", "0.2")), deprecated: contain_exactly("failball"), ) end @@ -1687,8 +1689,8 @@ def run_upgrade_with_failed_shared_prefetch(failed_download) cmd.upgrade_outdated_formulae!([]) expect(cmd.final_upgrade_summary).to have_attributes( - version_changes: contain_exactly("testball 0.1 -> 0.2"), - dependent_version_changes: contain_exactly("upgraded-dependent 0.1 -> 0.2"), + version_changes: contain_exactly(version_change("testball", "0.1", "0.2")), + dependent_version_changes: contain_exactly(version_change("upgraded-dependent", "0.1", "0.2")), ) end diff --git a/Library/Homebrew/test/install_spec.rb b/Library/Homebrew/test/install_spec.rb index 01c5ad258dcda..71029de42ff5c 100644 --- a/Library/Homebrew/test/install_spec.rb +++ b/Library/Homebrew/test/install_spec.rb @@ -203,7 +203,7 @@ described_class.install_formulae([installer], dry_run: true) end.to output(<<~EOS).to_stdout ==> Would install 1 dependency for requested: - dependency + dependency 1.0 EOS end end @@ -487,7 +487,11 @@ instance_double(Dependency, to_formula: installed), ] - expect { described_class.print_dry_run_dependencies(Testball.new, deps, &:name) } + expect do + described_class.print_dry_run_dependencies(Testball.new, deps) do |dep| + version_change(dep.name, "1.0") + end + end .to output(/Would install 1 dependency.*fresh-dep.*Would upgrade 1 dependency.*installed-dep/m).to_stdout end end diff --git a/Library/Homebrew/test/spec_helper.rb b/Library/Homebrew/test/spec_helper.rb index d6883da4b8b8d..2105edd3057e7 100644 --- a/Library/Homebrew/test/spec_helper.rb +++ b/Library/Homebrew/test/spec_helper.rb @@ -55,6 +55,7 @@ require "test/support/helper/mktmpdir" require "test/support/helper/subcommand" require "test/support/helper/test_each" +require "test/support/helper/version_change" require "test/support/helper/spec/shared_context/homebrew_cask" if OS.mac? require "test/support/helper/spec/shared_context/integration_test" @@ -172,6 +173,7 @@ def ensure_test_dependency!(available, message) config.include(Test::Helper::Cask) config.include(Test::Helper::Fixtures) config.include(Test::Helper::Formula) + config.include(Test::Helper::VersionChange) config.include(Test::Helper::MkTmpDir) config.include(Test::Helper::Subcommand) config.include(Test::Helper::Dependencies) diff --git a/Library/Homebrew/test/support/helper/version_change.rb b/Library/Homebrew/test/support/helper/version_change.rb new file mode 100644 index 0000000000000..8bce940bfa61b --- /dev/null +++ b/Library/Homebrew/test/support/helper/version_change.rb @@ -0,0 +1,26 @@ +# typed: strict +# frozen_string_literal: true + +require "version_change" + +module Test + module Helper + module VersionChange + # Build a {::VersionChange}: `version_change("name", "1.0", "2.0")` for an upgrade + # or `version_change("name", "1.0")` for a single version. + sig { + params( + name: String, + old_version: String, + new_version: T.nilable(String), + size: T.nilable(String), + ).returns(::VersionChange) + } + def version_change(name, old_version, new_version = nil, size: nil) + return ::VersionChange.new(name:, new_version: old_version, size:) unless new_version + + ::VersionChange.new(name:, old_version:, new_version:, size:) + end + end + end +end diff --git a/Library/Homebrew/test/upgrade_spec.rb b/Library/Homebrew/test/upgrade_spec.rb index 875a3cc8f96b0..391e2497e9814 100644 --- a/Library/Homebrew/test/upgrade_spec.rb +++ b/Library/Homebrew/test/upgrade_spec.rb @@ -10,48 +10,6 @@ require "test/support/fixtures/testball" RSpec.describe Homebrew::Upgrade do - describe "::format_upgrade_summary" do - it "aligns a large mixed list of package names and versions" do - upgrades = [ - "sqlite 3.53.1 -> 3.53.2 (2.4MB)", - "docker 29.5.2 -> 29.6.0 (9.3MB)", - "gh 2.93.0 -> 2.95.0 (13.4MB)", - "python@3.14 3.14.5 -> 3.14.6 (19.2MB)", - "pnpm 11.5.1 -> 11.8.0 (4MB)", - "usage 3.4.0 -> 3.5.2 (2.9MB)", - "certifi 2026.5.20 -> 2026.6.17 (5.7KB)", - "libvmaf 3.1.0 -> 3.2.0 (1.2MB)", - "kubernetes-cli 1.36.1 -> 1.36.2 (18.2MB)", - "jq 1.8.1 -> 1.8.2 (441KB)", - "mise 2026.6.0 -> 2026.6.11 (34.8MB)", - "sdl2 2.32.70 (636.8KB)", - "opencode-desktop 1.14.48 -> 1.17.9", - "slack 4.48.102 -> 4.50.140", - "spotify 1.2.84.476 -> 1.2.92.148", - "visual-studio-code 1.111.0 -> 1.125.1", - ] - - expect(described_class.format_upgrade_summary(upgrades)).to eq([ - "sqlite 3.53.1 -> 3.53.2 (2.4MB)", - "docker 29.5.2 -> 29.6.0 (9.3MB)", - "gh 2.93.0 -> 2.95.0 (13.4MB)", - "python@3.14 3.14.5 -> 3.14.6 (19.2MB)", - "pnpm 11.5.1 -> 11.8.0 (4MB)", - "usage 3.4.0 -> 3.5.2 (2.9MB)", - "certifi 2026.5.20 -> 2026.6.17 (5.7KB)", - "libvmaf 3.1.0 -> 3.2.0 (1.2MB)", - "kubernetes-cli 1.36.1 -> 1.36.2 (18.2MB)", - "jq 1.8.1 -> 1.8.2 (441KB)", - "mise 2026.6.0 -> 2026.6.11 (34.8MB)", - "sdl2 2.32.70 (636.8KB)", - "opencode-desktop 1.14.48 -> 1.17.9", - "slack 4.48.102 -> 4.50.140", - "spotify 1.2.84.476 -> 1.2.92.148", - "visual-studio-code 1.111.0 -> 1.125.1", - ]) - end - end - describe "::upgrade_formula" do it "shows the version transition for an unlinked dependency installed at an older version" do python = formula("python@3.14") do diff --git a/Library/Homebrew/test/utils/formatter_spec.rb b/Library/Homebrew/test/utils/formatter_spec.rb index 32c23cad013e2..6c2d05b08addd 100644 --- a/Library/Homebrew/test/utils/formatter_spec.rb +++ b/Library/Homebrew/test/utils/formatter_spec.rb @@ -35,4 +35,59 @@ expect(many_first_row.index("item3")).to eq(few_first_row.index("b")) end end + + describe "::version_changes" do + it "leaves a single entry on one line" do + expect(described_class.version_changes([version_change("b4", "0.15", "0.16")])).to eq(["b4 0.15 -> 0.16"]) + end + + it "aligns multiple entries" do + changes = [version_change("cffi", "2.1.1"), version_change("libgit2", "1.9", "1.10")] + + expect(described_class.version_changes(changes)).to eq([ + "cffi 2.1.1", + "libgit2 1.9 -> 1.10", + ]) + end + + it "aligns a large mixed list of package names and versions" do + upgrades = [ + version_change("sqlite", "3.53.1", "3.53.2", size: "2.4MB"), + version_change("docker", "29.5.2", "29.6.0", size: "9.3MB"), + version_change("gh", "2.93.0", "2.95.0", size: "13.4MB"), + version_change("python@3.14", "3.14.5", "3.14.6", size: "19.2MB"), + version_change("pnpm", "11.5.1", "11.8.0", size: "4MB"), + version_change("usage", "3.4.0", "3.5.2", size: "2.9MB"), + version_change("certifi", "2026.5.20", "2026.6.17", size: "5.7KB"), + version_change("libvmaf", "3.1.0", "3.2.0", size: "1.2MB"), + version_change("kubernetes-cli", "1.36.1", "1.36.2", size: "18.2MB"), + version_change("jq", "1.8.1", "1.8.2", size: "441KB"), + version_change("mise", "2026.6.0", "2026.6.11", size: "34.8MB"), + version_change("sdl2", "2.32.70", size: "636.8KB"), + version_change("opencode-desktop", "1.14.48", "1.17.9"), + version_change("slack", "4.48.102", "4.50.140"), + version_change("spotify", "1.2.84.476", "1.2.92.148"), + version_change("visual-studio-code", "1.111.0", "1.125.1"), + ] + + expect(described_class.version_changes(upgrades)).to eq([ + "sqlite 3.53.1 -> 3.53.2 (2.4MB)", + "docker 29.5.2 -> 29.6.0 (9.3MB)", + "gh 2.93.0 -> 2.95.0 (13.4MB)", + "python@3.14 3.14.5 -> 3.14.6 (19.2MB)", + "pnpm 11.5.1 -> 11.8.0 (4MB)", + "usage 3.4.0 -> 3.5.2 (2.9MB)", + "certifi 2026.5.20 -> 2026.6.17 (5.7KB)", + "libvmaf 3.1.0 -> 3.2.0 (1.2MB)", + "kubernetes-cli 1.36.1 -> 1.36.2 (18.2MB)", + "jq 1.8.1 -> 1.8.2 (441KB)", + "mise 2026.6.0 -> 2026.6.11 (34.8MB)", + "sdl2 2.32.70 (636.8KB)", + "opencode-desktop 1.14.48 -> 1.17.9", + "slack 4.48.102 -> 4.50.140", + "spotify 1.2.84.476 -> 1.2.92.148", + "visual-studio-code 1.111.0 -> 1.125.1", + ]) + end + end end diff --git a/Library/Homebrew/test/version_change_spec.rb b/Library/Homebrew/test/version_change_spec.rb new file mode 100644 index 0000000000000..8b60a59f8a2d5 --- /dev/null +++ b/Library/Homebrew/test/version_change_spec.rb @@ -0,0 +1,17 @@ +# typed: strict +# frozen_string_literal: true + +require "version_change" + +RSpec.describe VersionChange do + it "deduplicates equal changes" do + changes = [version_change("gh", "2.93.0", "2.95.0"), version_change("gh", "2.93.0", "2.95.0")] + + expect(changes.uniq.count).to eq(1) + end + + it "distinguishes changes with different sizes" do + expect(version_change("gh", "2.93.0", "2.95.0", + size: "13.4MB")).not_to eq(version_change("gh", "2.93.0", "2.95.0")) + end +end diff --git a/Library/Homebrew/upgrade.rb b/Library/Homebrew/upgrade.rb index aa923cdcc0bcb..17623b917d86c 100644 --- a/Library/Homebrew/upgrade.rb +++ b/Library/Homebrew/upgrade.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "utils/text" +require "version_change" require "reinstall" require "formula_installer" @@ -35,45 +36,11 @@ def installed_version(formula) end # Describe a formula's version change for an upgrade summary. - sig { params(formula: Formula).returns(String) } - def formula_upgrade_description(formula) + sig { params(formula: Formula).returns(VersionChange) } + def formula_version_change(formula) current_version = installed_version(formula) - if current_version && current_version != formula.pkg_version - "#{formula.full_specified_name} #{current_version} -> #{formula.pkg_version}" - else - "#{formula.full_specified_name} #{formula.pkg_version}" - end - end - - sig { params(upgrades: T::Array[String]).returns(T::Array[String]) } - def format_upgrade_summary(upgrades) - return upgrades if upgrades.size < 2 - - name_width = upgrades.map { |upgrade| upgrade.split(" ", 2).fetch(0).length }.max - name_width ||= 0 - old_version_width = upgrades.filter_map do |upgrade| - versions = upgrade.split(" ", 2).fetch(1, "") - next unless versions.include?(" -> ") - - versions.split(" -> ", 2).fetch(0).length - end.max - old_version_width ||= 0 - - upgrades.map do |upgrade| - parts = upgrade.split(" ", 2) - name = parts.fetch(0) - versions = parts.fetch(1, "") - next name if versions.blank? - - if versions.include?(" -> ") - version_parts = versions.split(" -> ", 2) - old_version = version_parts.fetch(0) - new_version = version_parts.fetch(1) - "#{name.ljust(name_width)} #{old_version.ljust(old_version_width)} -> #{new_version}" - else - "#{name.ljust(name_width)} #{versions}" - end - end + old_version = current_version.to_s if current_version && current_version != formula.pkg_version + VersionChange.new(name: formula.full_specified_name, old_version:, new_version: formula.pkg_version.to_s) end sig { @@ -435,15 +402,7 @@ def upgrade_dependents(deps, formulae, ohai "#{upgrade_verb} #{Utils.pluralize("dependent", upgradeable.count, include_count: true)} of upgraded #{formula_plural}:" puts_no_installed_dependents_check_disable_message_if_not_already! - formulae_upgrades = upgradeable.map do |f| - name = f.full_specified_name - if f.optlinked? - "#{name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}" - else - "#{name} #{f.pkg_version}" - end - end - puts format_upgrade_summary(formulae_upgrades).join("\n") + puts Formatter.version_changes(upgradeable.map { |f| formula_version_change(f) }).join("\n") end if !dry_run && dependent_installers.present? @@ -559,7 +518,7 @@ def upgrade_formula(formula_installer, dry_run: false, verbose: false, skip_form if dry_run Install.print_dry_run_dependencies(formula, formula_installer.compute_dependencies, skip_formula_names:, dependency_summary:) do |f| - formula_upgrade_description(f) + formula_version_change(f) end return true end diff --git a/Library/Homebrew/utils/formatter.rb b/Library/Homebrew/utils/formatter.rb index 1ffd31ea8c7fc..c0267d6fce52a 100644 --- a/Library/Homebrew/utils/formatter.rb +++ b/Library/Homebrew/utils/formatter.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "utils/tty" +require "version_change" # Helper module for formatting output. # @@ -127,6 +128,27 @@ def self.prefix(prefix, string, color) end private_class_method :prefix + # Align version changes into columns: `name old -> new` or `name version`, followed by the download size + # like `(2.4MB)` in its own column. + sig { params(changes: T::Array[VersionChange]).returns(T::Array[String]) } + def self.version_changes(changes) + old_width = changes.map { |change| (change.old_version || change.new_version).length }.max || 0 + versions = changes.map do |change| + old_version = change.old_version + next change.new_version unless old_version + + "#{old_version.ljust(old_width)} -> #{change.new_version}" + end + name_width = changes.map { |change| change.name.length }.max || 0 + versions_width = versions.map(&:length).max || 0 + separator = (changes.one? ? " " : " ") + + changes.each_with_index.map do |change, index| + size = "(#{change.size})" if change.size + [change.name.ljust(name_width), versions.fetch(index).ljust(versions_width), size].join(separator).rstrip + end + end + # Layout objects in columns that fit the current terminal width. # # @api internal diff --git a/Library/Homebrew/version_change.rb b/Library/Homebrew/version_change.rb new file mode 100644 index 0000000000000..02208189e1e92 --- /dev/null +++ b/Library/Homebrew/version_change.rb @@ -0,0 +1,28 @@ +# typed: strict +# frozen_string_literal: true + +# A package version change as listed in install and upgrade summaries. +class VersionChange < T::Struct + const :name, String + const :old_version, T.nilable(String), default: nil + const :new_version, String + # The download size, e.g. `2.4MB`. + const :size, T.nilable(String), default: nil + + sig { params(other: BasicObject).returns(T::Boolean) } + def ==(other) + case other + when VersionChange then values == other.values + else false + end + end + alias eql? == + + sig { returns(Integer) } + def hash = values.hash + + protected + + sig { returns(T::Array[T.nilable(String)]) } + def values = [name, old_version, new_version, size] +end