Skip to content

Commit 736fea2

Browse files
committed
upgrade: add final summary
- Make `brew upgrade` easier to scan after noisy output. - Preserve existing progress output while narrowing the final view. - Reuse selected formula and cask upgrade data instead of re-querying.
1 parent 0f453c8 commit 736fea2

4 files changed

Lines changed: 208 additions & 5 deletions

File tree

Library/Homebrew/cask/upgrade.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ def self.greedy_casks
2929
greedy: T.nilable(T::Boolean),
3030
greedy_latest: T.nilable(T::Boolean),
3131
greedy_auto_updates: T.nilable(T::Boolean),
32+
summary_disabled: T.nilable(T::Array[String]),
3233
).returns(T::Array[Cask])
3334
}
3435
def self.outdated_casks(casks, args:, force:, quiet:,
35-
greedy: false, greedy_latest: false, greedy_auto_updates: false)
36+
greedy: false, greedy_latest: false, greedy_auto_updates: false,
37+
summary_disabled: nil)
3638
# Validate mutually exclusive opt-in/opt-out env vars before we start
3739
# selecting casks so `brew upgrade` errors consistently.
3840
Homebrew::EnvConfig.upgrade_auto_updates_casks?
@@ -41,6 +43,7 @@ def self.outdated_casks(casks, args:, force:, quiet:,
4143
if casks.empty?
4244
Caskroom.casks(config: Config.from_args(args)).select do |cask|
4345
if cask.disabled?
46+
summary_disabled&.push(cask.full_name)
4447
opoo "Not upgrading #{cask.token}, it is #{DeprecateDisable.message(cask)}" unless quiet
4548
next false
4649
end
@@ -54,6 +57,7 @@ def self.outdated_casks(casks, args:, force:, quiet:,
5457
raise CaskNotInstalledError, cask if !cask.installed? && !force
5558

5659
if cask.disabled?
60+
summary_disabled&.push(cask.full_name)
5761
opoo "Not upgrading #{cask.token}, it is #{DeprecateDisable.message(cask)}" unless quiet
5862
next false
5963
end
@@ -98,6 +102,9 @@ def self.show_upgrade_summary(cask_upgrades, dry_run: false)
98102
skip_prefetch: T::Boolean,
99103
show_upgrade_summary: T::Boolean,
100104
download_queue: T.nilable(Homebrew::DownloadQueue),
105+
summary_upgrades: T.nilable(T::Array[String]),
106+
summary_deprecated: T.nilable(T::Array[String]),
107+
summary_disabled: T.nilable(T::Array[String]),
101108
).returns(T::Boolean)
102109
}
103110
def self.upgrade_casks!(
@@ -116,12 +123,16 @@ def self.upgrade_casks!(
116123
require_sha: nil,
117124
skip_prefetch: false,
118125
show_upgrade_summary: true,
119-
download_queue: nil
126+
download_queue: nil,
127+
summary_upgrades: nil,
128+
summary_deprecated: nil,
129+
summary_disabled: nil
120130
)
121131
quarantine = true if quarantine.nil?
122132

123133
outdated_casks =
124-
self.outdated_casks(casks, args:, greedy:, greedy_latest:, greedy_auto_updates:, force:, quiet:)
134+
self.outdated_casks(casks, args:, greedy:, greedy_latest:, greedy_auto_updates:, force:, quiet:,
135+
summary_disabled:)
125136

126137
manual_installer_casks = outdated_casks.select do |cask|
127138
cask.artifacts.any? do |artifact|
@@ -187,6 +198,10 @@ def self.upgrade_casks!(
187198
cask_upgrades = upgradable_casks.map do |(old_cask, new_cask)|
188199
"#{new_cask.full_name} #{old_cask.version} -> #{new_cask.version}"
189200
end
201+
summary_upgrades&.concat(cask_upgrades)
202+
summary_deprecated&.concat(upgradable_casks.filter_map do |(_, new_cask)|
203+
new_cask.full_name if new_cask.deprecated?
204+
end)
190205

191206
created_download_queue = T.let(false, T::Boolean)
192207
download_queue ||= if !dry_run && !skip_prefetch

Library/Homebrew/cmd/upgrade.rb

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ class FormulaeUpgradeContext < T::Struct
1717
const :formulae_to_install, T::Array[Formula]
1818
const :formulae_installer, T::Array[FormulaInstaller]
1919
const :dependants, Homebrew::Upgrade::Dependents
20+
const :pinned_formulae, T::Array[Formula]
21+
end
22+
23+
class FinalUpgradeSummary < T::Struct
24+
prop :version_changes, T::Array[String], default: []
25+
prop :pinned_formulae, T::Array[String], default: []
26+
prop :deprecated, T::Array[String], default: []
27+
prop :disabled, T::Array[String], default: []
28+
prop :source_build_formulae, T::Array[String], default: []
2029
end
2130

2231
cmd_args do
@@ -147,6 +156,7 @@ def run
147156
prefetched_formulae_upgrades = T.let([], T::Array[String])
148157
prefetched_cask_names = T.let([], T::Array[String])
149158
prefetched_cask_upgrades = T.let([], T::Array[String])
159+
@final_upgrade_summary = T.let(FinalUpgradeSummary.new, T.nilable(FinalUpgradeSummary))
150160

151161
if args.named.present?
152162
args.named.to_formulae_and_casks_and_unavailable(method: :resolve).each do |item|
@@ -237,6 +247,8 @@ def run
237247
Homebrew::Reinstall.reinstall_pkgconf_if_needed!(dry_run: args.dry_run?)
238248

239249
Homebrew.messages.display_messages(display_times: args.display_times?)
250+
251+
show_final_upgrade_summary
240252
end
241253

242254
private
@@ -324,7 +336,16 @@ def formulae_upgrade_context(formulae, show_upgrade_summary: true)
324336
verbose: args.verbose?,
325337
)
326338

327-
return if formulae_installer.blank?
339+
if formulae_installer.blank?
340+
return if pinned.blank?
341+
342+
return FormulaeUpgradeContext.new(
343+
formulae_to_install:,
344+
formulae_installer: formulae_installer,
345+
dependants: Homebrew::Upgrade::Dependents.new(upgradeable: [], pinned: [], skipped: []),
346+
pinned_formulae: pinned,
347+
)
348+
end
328349

329350
dependants = Upgrade.dependants(
330351
formulae_to_install,
@@ -349,9 +370,78 @@ def formulae_upgrade_context(formulae, show_upgrade_summary: true)
349370
formulae_to_install:,
350371
formulae_installer: formulae_installer,
351372
dependants:,
373+
pinned_formulae: pinned,
352374
)
353375
end
354376

377+
sig { returns(FinalUpgradeSummary) }
378+
def final_upgrade_summary
379+
@final_upgrade_summary ||= T.let(FinalUpgradeSummary.new, T.nilable(FinalUpgradeSummary))
380+
@final_upgrade_summary
381+
end
382+
383+
sig { params(context: FormulaeUpgradeContext).void }
384+
def record_formula_upgrade_summary(context)
385+
summary = final_upgrade_summary
386+
summary.version_changes.concat(formula_upgrade_descriptions(context.formulae_installer.map(&:formula)))
387+
summary.version_changes.concat(formula_upgrade_descriptions(context.dependants.upgradeable))
388+
summary.pinned_formulae.concat((context.pinned_formulae + context.dependants.pinned).map do |formula|
389+
"#{formula.full_specified_name} #{formula.pkg_version}"
390+
end)
391+
392+
formulae = context.formulae_to_install + context.pinned_formulae +
393+
context.dependants.upgradeable + context.dependants.pinned
394+
summary.deprecated.concat(formulae.filter_map do |formula|
395+
formula.full_specified_name if formula.deprecated?
396+
end)
397+
summary.disabled.concat(formulae.filter_map do |formula|
398+
formula.full_specified_name if formula.disabled?
399+
end)
400+
summary.source_build_formulae.concat(context.formulae_installer.filter_map do |formula_installer|
401+
formula = formula_installer.formula
402+
next unless formula.tap&.official?
403+
next if formula_installer.pour_bottle?
404+
405+
formula.full_specified_name
406+
end)
407+
end
408+
409+
sig { void }
410+
def show_final_upgrade_summary
411+
summary = final_upgrade_summary
412+
return if summary.version_changes.empty? && summary.pinned_formulae.empty? &&
413+
summary.deprecated.empty? && summary.disabled.empty? && summary.source_build_formulae.empty?
414+
415+
oh1 "Final upgrade summary"
416+
show_final_upgrade_summary_section("Version changes:", summary.version_changes)
417+
show_final_upgrade_summary_section(
418+
args.dry_run? ? "Pinned formulae that would be skipped:" : "Pinned formulae skipped:",
419+
summary.pinned_formulae,
420+
)
421+
show_final_upgrade_summary_section("Deprecated formulae/casks involved:", summary.deprecated)
422+
show_final_upgrade_summary_section("Disabled formulae/casks involved:", summary.disabled)
423+
if args.dry_run?
424+
show_final_upgrade_summary_section(
425+
"Official tap formulae that would build from source:",
426+
summary.source_build_formulae,
427+
)
428+
else
429+
show_final_upgrade_summary_section(
430+
"Official tap formulae built from source:",
431+
summary.source_build_formulae,
432+
)
433+
end
434+
end
435+
436+
sig { params(title: String, items: T::Array[String]).void }
437+
def show_final_upgrade_summary_section(title, items)
438+
items = items.uniq
439+
return if items.empty?
440+
441+
puts title
442+
puts items.map { |item| " #{item}" }.join("\n")
443+
end
444+
355445
sig { params(formulae: T::Array[Formula]).returns(T::Array[String]) }
356446
def formula_upgrade_descriptions(formulae)
357447
formulae.map do |formula|
@@ -406,6 +496,7 @@ def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: f
406496
formulae_to_install: context.formulae_to_install,
407497
formulae_installer: valid_formula_installers,
408498
dependants: context.dependants,
499+
pinned_formulae: context.pinned_formulae,
409500
)
410501
return valid_formula_installers.present?
411502
end
@@ -432,6 +523,8 @@ def upgrade_outdated_formulae!(formulae, prefetch_only: false, use_prefetched: f
432523
verbose: args.verbose?
433524
)
434525

526+
record_formula_upgrade_summary(context)
527+
435528
@prefetched_formulae_upgrade_context = nil if use_prefetched_context
436529
true
437530
end
@@ -523,6 +616,9 @@ def upgrade_outdated_casks!(casks, skip_prefetch: false, show_upgrade_summary: t
523616
skip_prefetch:,
524617
show_upgrade_summary:,
525618
download_queue:,
619+
summary_upgrades: final_upgrade_summary.version_changes,
620+
summary_deprecated: final_upgrade_summary.deprecated,
621+
summary_disabled: final_upgrade_summary.disabled,
526622
args:,
527623
)
528624
rescue => e

Library/Homebrew/test/cask/upgrade_spec.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,24 @@ def write_info_plist(path, short_version:, bundle_version:)
154154
described_class.upgrade_casks!(dry_run: true, args:)
155155
end
156156

157+
it "records final cask upgrade summary details" do
158+
summary_upgrades = []
159+
summary_deprecated = []
160+
allow(local_caffeine).to receive(:deprecated?).and_return(true)
161+
162+
described_class.upgrade_casks!(
163+
local_caffeine,
164+
dry_run: true,
165+
show_upgrade_summary: false,
166+
summary_upgrades:,
167+
summary_deprecated:,
168+
args:,
169+
)
170+
171+
expect(summary_upgrades).to include("local-caffeine 1.2.2 -> 1.2.3")
172+
expect(summary_deprecated).to include("local-caffeine")
173+
end
174+
157175
it "would update only the Casks specified in the command line" do
158176
expect(described_class).not_to receive(:upgrade_cask)
159177
expect(described_class).to receive(:show_upgrade_summary)
@@ -476,12 +494,14 @@ def write_info_plist(path, short_version:, bundle_version:)
476494
cask = Cask::CaskLoader.load(cask_path("livecheck/livecheck-disabled"))
477495
InstallHelper.stub_cask_installation(cask)
478496
allow(cask).to receive(:outdated?).with(greedy: true).and_return(true)
497+
summary_disabled = []
479498

480499
expect(described_class).not_to receive(:upgrade_cask)
481500

482501
expect do
483-
described_class.upgrade_casks!(cask, dry_run: true, args:)
502+
described_class.upgrade_casks!(cask, dry_run: true, summary_disabled:, args:)
484503
end.to output(/Not upgrading livecheck-disabled, it is disabled/).to_stderr
504+
expect(summary_disabled).to eq(["livecheck-disabled"])
485505
end
486506

487507
context "when an upgrade failed" do

Library/Homebrew/test/cmd/upgrade_spec.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,5 +204,77 @@
204204
.to not_to_output(/Unexpected method 'discontinued' called during caveats on Cask local-caffeine\./).to_stderr
205205
end
206206

207+
it "prints a narrow final upgrade summary" do
208+
cmd = described_class.new(["--dry-run"])
209+
summary = described_class::FinalUpgradeSummary.new(
210+
version_changes: ["testball 0.1 -> 0.2"],
211+
pinned_formulae: ["pinnedball 1.0"],
212+
deprecated: ["oldball"],
213+
disabled: ["disabledball"],
214+
source_build_formulae: ["sourceball"],
215+
)
216+
217+
allow(cmd).to receive(:final_upgrade_summary).and_return(summary)
218+
219+
expect { cmd.send(:show_final_upgrade_summary) }.to output(<<~EOS).to_stdout
220+
==> Final upgrade summary
221+
Version changes:
222+
testball 0.1 -> 0.2
223+
Pinned formulae that would be skipped:
224+
pinnedball 1.0
225+
Deprecated formulae/casks involved:
226+
oldball
227+
Disabled formulae/casks involved:
228+
disabledball
229+
Official tap formulae that would build from source:
230+
sourceball
231+
EOS
232+
end
233+
234+
it "records final formula upgrade summary details" do
235+
formula = formula("testball") do
236+
url "https://brew.sh/testball-0.2"
237+
end
238+
pinned = formula("pinnedball") do
239+
url "https://brew.sh/pinnedball-1.0"
240+
end
241+
deprecated = formula("oldball") do
242+
url "https://brew.sh/oldball-1.0"
243+
deprecate! date: "2020-01-01", because: :unmaintained
244+
end
245+
disabled = formula("disabledball") do
246+
url "https://brew.sh/disabledball-1.0"
247+
disable! date: "2020-01-01", because: :unsupported
248+
end
249+
source_build = formula("sourceball") do
250+
url "https://brew.sh/sourceball-1.0"
251+
end
252+
old_keg = HOMEBREW_CELLAR/"testball/0.1"
253+
old_keg.mkpath
254+
allow(formula).to receive_messages(optlinked?: true, opt_prefix: old_keg)
255+
256+
cmd = described_class.new([])
257+
context = described_class::FormulaeUpgradeContext.new(
258+
formulae_to_install: [formula, deprecated, disabled, source_build],
259+
formulae_installer: [
260+
FormulaInstaller.new(formula),
261+
FormulaInstaller.new(deprecated),
262+
FormulaInstaller.new(disabled),
263+
FormulaInstaller.new(source_build, build_from_source_formulae: [source_build.full_name]),
264+
],
265+
dependants: Homebrew::Upgrade::Dependents.new(upgradeable: [], pinned: [], skipped: []),
266+
pinned_formulae: [pinned],
267+
)
268+
269+
cmd.send(:record_formula_upgrade_summary, context)
270+
summary = cmd.send(:final_upgrade_summary)
271+
272+
expect(summary.version_changes).to include("testball 0.1 -> 0.2")
273+
expect(summary.pinned_formulae).to include("pinnedball 1.0")
274+
expect(summary.deprecated).to include("oldball")
275+
expect(summary.disabled).to include("disabledball")
276+
expect(summary.source_build_formulae).to include("sourceball")
277+
end
278+
207279
it_behaves_like "reinstall_pkgconf_if_needed"
208280
end

0 commit comments

Comments
 (0)