diff --git a/Library/Homebrew/cask/upgrade.rb b/Library/Homebrew/cask/upgrade.rb index e0c1980f4b046..379bb275e0bde 100644 --- a/Library/Homebrew/cask/upgrade.rb +++ b/Library/Homebrew/cask/upgrade.rb @@ -6,6 +6,7 @@ require "cask/quarantine" require "deprecate_disable" require "install" +require "upgrade" require "utils/output" module Cask @@ -92,7 +93,7 @@ def self.show_upgrade_summary(cask_upgrades, dry_run: false) verb = dry_run ? "Would upgrade" : "Upgrading" oh1 "#{verb} #{cask_upgrades.count} outdated #{::Utils.pluralize("package", cask_upgrades.count)}:" - puts cask_upgrades.join("\n") + puts Homebrew::Upgrade.format_upgrade_summary(cask_upgrades).join("\n") end sig { diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index 045f0f24e0707..55856109d6435 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -425,7 +425,8 @@ 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 formula_upgrade_descriptions(formulae_to_install).join("\n") if args.no_ask? + puts Upgrade.format_upgrade_summary(formula_upgrade_descriptions(formulae_to_install)).join("\n") if + args.no_ask? end Install.perform_preinstall_checks_once @@ -556,7 +557,7 @@ def show_final_upgrade_summary(dry_run: args.dry_run?) show_final_upgrade_summary_section( "#{dry_run ? "Would upgrade" : "Upgraded"} #{version_change_count} outdated " \ "#{Utils.pluralize("package", version_change_count)}", - summary.version_changes, + Upgrade.format_upgrade_summary(summary.version_changes), ) end if summary.pinned_formulae.present? diff --git a/Library/Homebrew/test/cmd/upgrade_spec.rb b/Library/Homebrew/test/cmd/upgrade_spec.rb index 2d25f9ef24496..ca82b37ffcc6a 100644 --- a/Library/Homebrew/test/cmd/upgrade_spec.rb +++ b/Library/Homebrew/test/cmd/upgrade_spec.rb @@ -149,6 +149,31 @@ def setup_pinned_dependency_upgrade .to output(/minimum-version-formula 1\.2\.2 -> 1\.2\.3/).to_stdout end + it "aligns formula-only no-ask upgrade summaries" do + write_formula "gh", <<~RUBY + url "https://brew.sh/gh-2.95.0" + RUBY + write_formula "visual-studio-code", <<~RUBY + url "https://brew.sh/visual-studio-code-1.125.1" + RUBY + install_formula_version "gh", "2.93.0", optlinked: true + install_formula_version "visual-studio-code", "1.111.0", optlinked: true + allow(Homebrew::Upgrade).to receive(:formula_installers).and_return([]) + allow(Homebrew::Cleanup).to receive(:periodic_clean!) + allow(Homebrew::Reinstall).to receive(:reinstall_pkgconf_if_needed!) + allow(Homebrew.messages).to receive(:display_messages) + + expected_summary = <<~EOS + ==> Upgrading 2 outdated packages: + gh 2.93.0 -> 2.95.0 + visual-studio-code 1.111.0 -> 1.125.1 + EOS + + expect do + described_class.new(["--yes", "--formula", "gh", "visual-studio-code"]).run + end.to output(a_string_starting_with(expected_summary)).to_stdout + end + it "does not upgrade a named formula installed at --minimum-version" do write_formula "minimum-version-formula", <<~RUBY url "https://brew.sh/minimum-version-formula-1.2.4" @@ -535,6 +560,37 @@ def setup_pinned_dependency_upgrade end.not_to output.to_stdout end + it "aligns dependent formula upgrade summaries" do + formula = formula("sqlite") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/sqlite-3.53.2.tar.gz" + end + dependants = Homebrew::Upgrade::Dependents.new( + upgradeable: [ + formula("gh") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/gh-2.95.0.tar.gz" + end, + formula("visual-studio-code") do + T.bind(self, T.class_of(Formula)) + url "https://brew.sh/visual-studio-code-1.125.1.tar.gz" + end, + ], + pinned: [], + skipped: [], + ) + + with_env(HOMEBREW_NO_ENV_HINTS: "1") do + expect do + Homebrew::Upgrade.upgrade_dependents(dependants, [formula], flags: [], dry_run: true) + end.to output(<<~EOS).to_stdout + ==> Would upgrade 2 dependents of upgraded formula: + gh 2.95.0 + visual-studio-code 1.125.1 + EOS + end + end + it "does not print aggregate package sizes" do cmd = described_class.new(["--dry-run"]) summary = Homebrew::Cmd::UpgradeCmd::FinalUpgradeSummary.new( @@ -545,8 +601,8 @@ def setup_pinned_dependency_upgrade expect { cmd.send(:show_final_upgrade_summary) }.to output(<<~EOS).to_stdout ==> Would upgrade 2 outdated packages - testball 0.1 -> 0.2 (500B) - codex 1.0 -> 2.0 + testball 0.1 -> 0.2 (500B) + codex 1.0 -> 2.0 EOS end @@ -606,8 +662,8 @@ def setup_pinned_dependency_upgrade expect { cmd.run }.to output(<<~EOS).to_stdout ==> Upgrading 2 outdated packages: - deno 2.7.10 -> 2.7.11 - codex 0.117.0 -> 0.118.0 + deno 2.7.10 -> 2.7.11 + codex 0.117.0 -> 0.118.0 ==> Fetching downloads for: deno and codex EOS end @@ -705,8 +761,8 @@ def setup_pinned_dependency_upgrade expect { cmd.run }.to output(<<~EOS).to_stdout ==> Downloading Cask files ==> Upgrading 2 outdated packages: - deno 2.7.10 -> 2.7.11 - codex 0.117.0 -> 0.118.0 + deno 2.7.10 -> 2.7.11 + codex 0.117.0 -> 0.118.0 ==> Fetching downloads for: deno and codex EOS end @@ -751,8 +807,8 @@ def setup_pinned_dependency_upgrade expect { cmd.run }.to output(<<~EOS).to_stdout ==> Upgrading 2 outdated packages: - deno 2.7.10 -> 2.7.11 - codex 0.117.0 -> 0.118.0 + deno 2.7.10 -> 2.7.11 + codex 0.117.0 -> 0.118.0 ==> Fetching downloads for: deno and codex EOS end diff --git a/Library/Homebrew/test/upgrade_spec.rb b/Library/Homebrew/test/upgrade_spec.rb new file mode 100644 index 0000000000000..76c0e39d623a3 --- /dev/null +++ b/Library/Homebrew/test/upgrade_spec.rb @@ -0,0 +1,48 @@ +# typed: true +# frozen_string_literal: true + +require "upgrade" + +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 +end diff --git a/Library/Homebrew/upgrade.rb b/Library/Homebrew/upgrade.rb index a84e3459d8e31..e863c9443a088 100644 --- a/Library/Homebrew/upgrade.rb +++ b/Library/Homebrew/upgrade.rb @@ -22,6 +22,37 @@ class Dependents < T::Struct end class << self + 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 + end + sig { params( formulae_to_install: T::Array[Formula], flags: T::Array[String], dry_run: T::Boolean, @@ -303,7 +334,7 @@ def upgrade_dependents(deps, formulae, "#{name} #{f.pkg_version}" end end - puts formulae_upgrades.join("\n") + puts format_upgrade_summary(formulae_upgrades).join("\n") end return if upgradeable.blank?