brew/Library/Homebrew/cmd/update-report.rb
Mike McQuaid 42e9782dd6 update-report: cleanup git harder in migration.
Before stashing also try to cancel any current merge, rebase and
reset. This will prevent any data loss but also make the stash more
likely to succeed.
2016-09-18 20:25:18 +01:00

549 lines
17 KiB
Ruby

#: @hide_from_man_page
#: * `update_report`:
#: The Ruby implementation of `brew update`. Never called manually.
require "formula_versions"
require "migrator"
require "formulary"
require "descriptions"
require "cleanup"
require "utils"
module Homebrew
def update_preinstall_header
@header_already_printed ||= begin
ohai "Auto-updated Homebrew!" if ARGV.include?("--preinstall")
true
end
end
def update_report
HOMEBREW_REPOSITORY.cd do
analytics_message_displayed = \
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsmessage").chuzzle
analytics_disabled = \
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsdisabled").chuzzle
if analytics_message_displayed != "true" && analytics_disabled != "true" && !ENV["HOMEBREW_NO_ANALYTICS"]
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
ohai "Homebrew has enabled anonymous aggregate user behaviour analytics"
puts "Read the analytics documentation (and how to opt-out) here:"
puts " https://git.io/brew-analytics"
# Consider the message possibly missed if not a TTY.
if $stdout.tty?
safe_system "git", "config", "--local", "--replace-all", "homebrew.analyticsmessage", "true"
end
end
end
install_core_tap_if_necessary
hub = ReporterHub.new
updated = false
initial_revision = ENV["HOMEBREW_UPDATE_BEFORE"].to_s
current_revision = ENV["HOMEBREW_UPDATE_AFTER"].to_s
if initial_revision.empty? || current_revision.empty?
odie "update-report should not be called directly!"
end
if initial_revision != current_revision
update_preinstall_header
puts "Updated Homebrew from #{shorten_revision(initial_revision)} to #{shorten_revision(current_revision)}."
updated = true
end
updated_taps = []
Tap.each do |tap|
next unless tap.git?
begin
reporter = Reporter.new(tap)
rescue Reporter::ReporterRevisionUnsetError => e
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
next
end
if reporter.updated?
updated_taps << tap.name
hub.add(reporter)
end
end
unless updated_taps.empty?
update_preinstall_header
puts "Updated #{updated_taps.size} tap#{plural(updated_taps.size)} " \
"(#{updated_taps.join(", ")})."
updated = true
end
migrate_legacy_cache_if_necessary
migrate_legacy_keg_symlinks_if_necessary
if !updated
if !ARGV.include?("--preinstall") && !ENV["HOMEBREW_UPDATE_FAILED"]
puts "Already up-to-date."
end
else
if hub.empty?
puts "No changes to formulae."
else
hub.dump
hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each(&:migrate_formula_rename)
Descriptions.update_cache(hub)
end
puts if ARGV.include?("--preinstall")
end
link_completions_and_docs
Tap.each(&:link_manpages)
Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
# This should always be the last thing to run
migrate_legacy_repository_if_necessary
end
private
def shorten_revision(revision)
Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
end
def install_core_tap_if_necessary
core_tap = CoreTap.instance
return if core_tap.installed?
CoreTap.ensure_installed! quiet: false
revision = core_tap.git_head
ENV["HOMEBREW_UPDATE_BEFORE_HOMEBREW_HOMEBREW_CORE"] = revision
ENV["HOMEBREW_UPDATE_AFTER_HOMEBREW_HOMEBREW_CORE"] = revision
end
def migrate_legacy_cache_if_necessary
legacy_cache = Pathname.new "/Library/Caches/Homebrew"
return if HOMEBREW_CACHE.to_s == legacy_cache.to_s
return unless legacy_cache.directory?
return unless legacy_cache.readable_real?
migration_attempted_file = legacy_cache/".migration_attempted"
return if migration_attempted_file.exist?
return unless legacy_cache.writable_real?
FileUtils.touch migration_attempted_file
# Cleanup to avoid copying files unnecessarily
ohai "Cleaning up #{legacy_cache}..."
Cleanup.cleanup_cache legacy_cache
# This directory could have been compromised if it's world-writable/
# a symlink/owned by another user so don't copy files in those cases.
world_writable = legacy_cache.stat.mode & 0777 == 0777
return if world_writable
return if legacy_cache.symlink?
return if !legacy_cache.owned? && legacy_cache.lstat.uid.nonzero?
ohai "Migrating #{legacy_cache} to #{HOMEBREW_CACHE}..."
HOMEBREW_CACHE.mkpath
legacy_cache.cd do
legacy_cache.entries.each do |f|
next if [".", "..", ".migration_attempted"].include? f.to_s
begin
FileUtils.cp_r f, HOMEBREW_CACHE
rescue
@migration_failed ||= true
end
end
end
if @migration_failed
opoo <<-EOS.undent
Failed to migrate #{legacy_cache} to
#{HOMEBREW_CACHE}. Please do so manually.
EOS
else
ohai "Deleting #{legacy_cache}..."
FileUtils.rm_rf legacy_cache
if legacy_cache.exist?
FileUtils.touch migration_attempted_file
opoo <<-EOS.undent
Failed to delete #{legacy_cache}.
Please do so manually.
EOS
end
end
end
def migrate_legacy_repository_if_necessary
return unless HOMEBREW_PREFIX.to_s == "/usr/local"
return unless HOMEBREW_REPOSITORY.to_s == "/usr/local"
return unless ARGV.homebrew_developer?
ohai "Migrating HOMEBREW_REPOSITORY (please wait)..."
unless HOMEBREW_PREFIX.writable_real?
ofail <<-EOS.undent
#{HOMEBREW_PREFIX} is not writable.
You should change the ownership and permissions of #{HOMEBREW_PREFIX}
temporarily back to your user account so we can complete the Homebrew
repository migration:
sudo chown -R $(whoami) #{HOMEBREW_PREFIX}
EOS
return
end
new_homebrew_repository = Pathname.new "/usr/local/Homebrew"
if new_homebrew_repository.exist?
ofail <<-EOS.undent
#{new_homebrew_repository} already exists.
Please remove it manually or uninstall and reinstall Homebrew into a new
location as the migration cannot be done automatically.
EOS
return
end
new_homebrew_repository.mkpath
repo_files = HOMEBREW_REPOSITORY.cd do
Utils.popen_read("git ls-files").lines.map(&:chomp)
end
unless Utils.popen_read("git status --untracked-files=all --porcelain").empty?
HOMEBREW_REPOSITORY.cd do
quiet_system "git", "merge", "--abort"
quiet_system "git", "rebase", "--abort"
quiet_system "git", "reset", "--mixed"
safe_system "git", "-c", "user.email=brew-update@localhost",
"-c", "user.name=brew update",
"stash", "save", "--include-untracked"
end
stashed = true
end
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/.git", "#{new_homebrew_repository}/.git"
new_homebrew_repository.cd do
safe_system "git", "checkout", "--force", "."
safe_system "git", "stash", "pop" if stashed
end
if (HOMEBREW_REPOSITORY/"Library/Locks").exist?
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/Library/Locks", "#{new_homebrew_repository}/Library/Locks"
end
if (HOMEBREW_REPOSITORY/"Library/Taps").exist?
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/Library/Taps", "#{new_homebrew_repository}/Library/Taps"
end
unremovable_paths = []
extra_remove_paths = [".git", "Library/Locks", "Library/Taps",
"Library/Homebrew/test"]
(repo_files + extra_remove_paths).each do |file|
path = Pathname.new "#{HOMEBREW_REPOSITORY}/#{file}"
begin
FileUtils.rm_rf path
rescue Errno::EACCES
unremovable_paths << path
end
quiet_system "rmdir", "-p", path.parent if path.parent.exist?
end
unless unremovable_paths.empty?
ofail <<-EOS.undent
Could not remove old HOMEBREW_REPOSITORY paths!
Please do this manually with:
sudo rm -rf #{unremovable_paths.join " "}
EOS
end
(Keg::ALL_TOP_LEVEL_DIRECTORIES + ["Cellar"]).each do |dir|
FileUtils.mkdir_p "#{HOMEBREW_PREFIX}/#{dir}"
end
src = Pathname.new("#{new_homebrew_repository}/bin/brew")
dst = Pathname.new("#{HOMEBREW_PREFIX}/bin/brew")
begin
FileUtils.ln_s(src.relative_path_from(dst.parent), dst)
rescue Errno::EACCES, Errno::ENOENT
ofail <<-EOS.undent
Could not create symlink at #{dst}!
Please do this manually with:
sudo ln -sf #{src} #{dst}
sudo chown $(whoami) #{dst}
EOS
end
link_completions_and_docs(new_homebrew_repository)
ohai "Migrated HOMEBREW_REPOSITORY to #{new_homebrew_repository}!"
puts "You may now chown #{HOMEBREW_PREFIX} back to root:wheel."
rescue => e
ofail <<-EOS.undent
#{e}
#{Tty.white}Failed to migrate HOMEBREW_REPOSITORY to #{new_homebrew_repository}!
Please comment with your (new) error in issue:
#{Tty.em}https://github.com/Homebrew/brew/issues/987#{Tty.reset}
EOS
$stderr.puts e.backtrace
end
def link_completions_and_docs(repository = HOMEBREW_REPOSITORY)
return if HOMEBREW_PREFIX.to_s == repository.to_s
command = "brew update"
link_src_dst_dirs(repository/"etc/bash_completion.d",
HOMEBREW_PREFIX/"etc/bash_completion.d", command)
link_src_dst_dirs(repository/"share/doc/homebrew",
HOMEBREW_PREFIX/"share/doc/homebrew", command, link_dir: true)
link_src_dst_dirs(repository/"share/zsh/site-functions",
HOMEBREW_PREFIX/"share/zsh/site-functions", command)
link_path_manpages(repository/"share", command)
end
end
class Reporter
class ReporterRevisionUnsetError < RuntimeError
def initialize(var_name)
super "#{var_name} is unset!"
end
end
attr_reader :tap, :initial_revision, :current_revision
def initialize(tap)
@tap = tap
initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{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#{repo_var}"
@current_revision = ENV[current_revision_var].to_s
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
def report
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 unless dst.extname == ".rb"
if paths.any? { |p| tap.cask_file?(p) }
# Currently only need to handle Cask deletion/migration.
if status == "D"
# Have a dedicated report array for deleted casks.
@report[:DC] << tap.formula_file_to_name(src)
end
end
next unless paths.any? { |p| tap.formula_file?(p) }
case status
when "A", "D"
@report[status.to_sym] << tap.formula_file_to_name(src)
when "M"
begin
formula = Formulary.factory(tap.path/src)
new_version = formula.pkg_version
old_version = FormulaVersions.new(formula).formula_at_revision(@initial_revision, &:pkg_version)
next if new_version == old_version
rescue Exception => e
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
end
@report[:M] << 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[:D] << src_full_name
@report[:A] << dst_full_name
end
end
renamed_formulae = []
@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
if tap.core_tap?
new_full_name = new_name
else
new_full_name = "#{tap}/#{new_name}"
end
renamed_formulae << [old_full_name, new_full_name] if @report[:A].include? new_full_name
end
unless renamed_formulae.empty?
@report[:A] -= renamed_formulae.map(&:last)
@report[:D] -= renamed_formulae.map(&:first)
@report[:R] = renamed_formulae
end
@report
end
def updated?
initial_revision != current_revision
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.
# This means it is a Cask
if report[:DC].include? full_name
next unless (HOMEBREW_REPOSITORY/"Caskroom"/name).exist?
new_tap = Tap.fetch(new_tap_name)
new_tap.install unless new_tap.installed?
ohai "#{name} has been moved to Homebrew.", <<-EOS.undent
To uninstall the cask run:
brew cask uninstall --force #{name}
EOS
new_full_name = "#{new_tap_name}/#{name}"
next if (HOMEBREW_CELLAR/name.split("/").last).directory?
ohai "Installing #{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
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_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 unless 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 == "caskroom/cask"
if new_tap.installed? && (HOMEBREW_REPOSITORY/"Caskroom").directory?
ohai "#{name} has been moved to Homebrew Cask."
ohai "brew uninstall --force #{name}"
system HOMEBREW_BREW_FILE, "uninstall", "--force", name
ohai "brew prune"
system HOMEBREW_BREW_FILE, "prune"
ohai "brew cask install #{name}"
system HOMEBREW_BREW_FILE, "cask", "install", name
else
ohai "#{name} has been moved to Homebrew Cask.", <<-EOS.undent
To uninstall the formula and install the cask run:
brew uninstall --force #{name}
brew cask install #{name}
EOS
end
else
new_tap.install unless new_tap.installed?
# update tap for each Tab
tabs.each { |tab| tab.tap = new_tap }
tabs.each(&:write)
end
end
end
def migrate_formula_rename
report[:R].each do |old_full_name, new_full_name|
old_name = old_full_name.split("/").last
next unless (dir = HOMEBREW_CELLAR/old_name).directory? && !dir.subdirs.empty?
begin
f = Formulary.factory(new_full_name)
rescue Exception => e
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
next
end
begin
migrator = Migrator.new(f)
migrator.migrate
rescue Migrator::MigratorDifferentTapsError
rescue Exception => e
onoe e
end
end
end
private
def repo_var
@repo_var ||= tap.path.to_s
.strip_prefix(Tap::TAP_DIRECTORY.to_s)
.tr("^A-Za-z0-9", "_")
.upcase
end
def diff
Utils.popen_read(
"git", "-C", tap.path, "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
"-M85%", initial_revision, current_revision
)
end
end
class ReporterHub
attr_reader :reporters
def initialize
@hash = {}
@reporters = []
end
def select_formula(key)
@hash.fetch(key, [])
end
def add(reporter)
@reporters << reporter
report = reporter.report.delete_if { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
end
def empty?
@hash.empty?
end
def dump
# Key Legend: Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
dump_formula_report :A, "New Formulae"
dump_formula_report :M, "Updated Formulae"
dump_formula_report :R, "Renamed Formulae"
dump_formula_report :D, "Deleted Formulae"
end
private
def dump_formula_report(key, title)
formulae = select_formula(key).sort.map do |name, new_name|
# Format list items of renamed formulae
if key == :R
name = pretty_installed(name) if installed?(name)
new_name = pretty_installed(new_name) if installed?(new_name)
"#{name} -> #{new_name}"
else
installed?(name) ? pretty_installed(name) : name
end
end
unless formulae.empty?
# Dump formula list.
ohai title
puts_columns(formulae)
end
end
def installed?(formula)
(HOMEBREW_CELLAR/formula.split("/").last).directory?
end
end