/
update-report.rb
974 lines (806 loc) 路 31.3 KB
/
update-report.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
# typed: true
# frozen_string_literal: true
require "migrator"
require "formulary"
require "cask/cask_loader"
require "cask/migrator"
require "descriptions"
require "cleanup"
require "description_cache_store"
require "cli/parser"
require "settings"
require "linuxbrew-core-migration"
module Homebrew
module_function
def auto_update_header(args:)
@auto_update_header ||= begin
ohai "Auto-updated Homebrew!" if args.auto_update?
true
end
end
sig { returns(CLI::Parser) }
def update_report_args
Homebrew::CLI::Parser.new 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
end
def update_report
return output_update_report if $stdout.tty?
redirect_stdout($stderr) do
output_update_report
end
end
def output_update_report
args = update_report_args.parse
# 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 args: args
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.each do |tap|
next if !tap.git? || tap.git_repo.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 = Tab.for_keg(keg)
# 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
onoe "#{e.message}\n#{Utils::Backtrace.clean(e)&.join("\n")}" if Homebrew::EnvConfig.developer?
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 args: args
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.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?
return if CoreTap.instance.installed?
CoreTap.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_repo.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"
api_auto_update_secs = Homebrew::EnvConfig.api_auto_update_secs.to_i
api_auto_update_secs_default = Homebrew::EnvConfig::ENVS.fetch(:HOMEBREW_API_AUTO_UPDATE_SECS).fetch(:default)
auto_update_secs_set = api_auto_update_secs.positive? && api_auto_update_secs != api_auto_update_secs_default
no_auto_update_set = Homebrew::EnvConfig.no_auto_update? &&
!ENV["HOMEBREW_GITHUB_HOSTED_RUNNER"] &&
!ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"]
no_install_from_api_set = Homebrew::EnvConfig.no_install_from_api? &&
!Homebrew::EnvConfig.automatically_set_no_install_from_api?
return if !no_auto_update_set && !no_install_from_api_set && !auto_update_secs_set
ohai "You have set:"
puts " HOMEBREW_NO_AUTO_UPDATE" if no_auto_update_set
puts " HOMEBREW_API_AUTO_UPDATE_SECS" if auto_update_secs_set
puts " HOMEBREW_NO_INSTALL_FROM_API" if no_install_from_api_set
puts "but we have dramatically sped up and fixed many bugs in the way we do Homebrew updates since."
puts "Please consider unsetting these and tweaking the values based on the new behaviour."
puts "\n\n"
# Consider the message possibly missed if not a TTY.
Settings.write "installfromapimessage", true if $stdout.tty?
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}"
@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}"
@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
onoe "#{e.message}\n#{Utils::Backtrace.clean(e)&.join("\n")}" if Homebrew::EnvConfig.developer?
end
next
end
next unless (dir = HOMEBREW_CELLAR/name).exist? # skip if formula is not installed.
tabs = dir.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
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: 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 = /^(---|\+\+\+) /.freeze
add_delete_characters = ["+", "-"].freeze
diff_output.lines.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.compact.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: 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)
report_all = ENV["HOMEBREW_UPDATE_REPORT_ALL_FORMULAE"].present?
if report_all && !Homebrew::EnvConfig.no_install_from_api?
odisabled "HOMEBREW_UPDATE_REPORT_ALL_FORMULAE"
opoo "This will not report all formulae because Homebrew cannot get this data from the API."
report_all = false
end
unless Homebrew::EnvConfig.no_update_report_new?
dump_new_formula_report
dump_new_cask_report
end
if report_all
dump_renamed_formula_report
dump_renamed_cask_report
end
dump_deleted_formula_report(report_all)
dump_deleted_cask_report(report_all)
outdated_formulae = []
outdated_casks = []
if report_all
if auto_update
if (changed_formulae = select_formula_or_cask(:M).count) && changed_formulae.positive?
ohai "Modified Formulae",
"Modified #{Utils.pluralize("formula", changed_formulae, plural: "e", include_count: true)}."
end
if (changed_casks = select_formula_or_cask(:MC).count) && changed_casks.positive?
ohai "Modified Casks", "Modified #{Utils.pluralize("cask", changed_casks, include_count: true)}."
end
else
dump_modified_formula_report
dump_modified_cask_report
end
else
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
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.map do |name|
name.split("/").last unless cask_installed?(name)
end.compact
output_dump_formula_or_cask_report "New Casks", casks
end
def dump_renamed_formula_report
formulae = select_formula_or_cask(:R).sort.map do |name, new_name|
name = pretty_installed(name) if installed?(name)
new_name = pretty_installed(new_name) if installed?(new_name)
"#{name} -> #{new_name}"
end
output_dump_formula_or_cask_report "Renamed Formulae", formulae
end
def dump_renamed_cask_report
casks = select_formula_or_cask(:RC).sort.map do |name, new_name|
name = pretty_installed(name) if installed?(name)
new_name = pretty_installed(new_name) if installed?(new_name)
"#{name} -> #{new_name}"
end
output_dump_formula_or_cask_report "Renamed Casks", casks
end
def dump_deleted_formula_report(report_all)
formulae = select_formula_or_cask(:D).sort.map do |name|
if installed?(name)
pretty_uninstalled(name)
elsif report_all
name
end
end.compact
title = if report_all
"Deleted Formulae"
else
"Deleted Installed Formulae"
end
output_dump_formula_or_cask_report title, formulae
end
def dump_deleted_cask_report(report_all)
casks = select_formula_or_cask(:DC).sort.map do |name|
name = name.split("/").last
if cask_installed?(name)
pretty_uninstalled(name)
elsif report_all
name
end
end.compact
title = if report_all
"Deleted Casks"
else
"Deleted Installed Casks"
end
output_dump_formula_or_cask_report title, casks
end
def dump_modified_formula_report
formulae = select_formula_or_cask(:M).sort.map do |name|
if installed?(name)
if outdated?(name)
pretty_outdated(name)
else
pretty_installed(name)
end
else
name
end
end
output_dump_formula_or_cask_report "Modified Formulae", formulae
end
def dump_modified_cask_report
casks = select_formula_or_cask(:MC).sort.map do |name|
name = name.split("/").last
if cask_installed?(name)
if cask_outdated?(name)
pretty_outdated(name)
else
pretty_installed(name)
end
else
name
end
end
output_dump_formula_or_cask_report "Modified 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