aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cmd/update-ruby.rb
diff options
context:
space:
mode:
Diffstat (limited to 'Library/Homebrew/cmd/update-ruby.rb')
-rw-r--r--Library/Homebrew/cmd/update-ruby.rb492
1 files changed, 492 insertions, 0 deletions
diff --git a/Library/Homebrew/cmd/update-ruby.rb b/Library/Homebrew/cmd/update-ruby.rb
new file mode 100644
index 000000000..98294bca8
--- /dev/null
+++ b/Library/Homebrew/cmd/update-ruby.rb
@@ -0,0 +1,492 @@
+require "cmd/tap"
+require "diagnostic"
+require "formula_versions"
+require "migrator"
+require "formulary"
+require "descriptions"
+
+module Homebrew
+ def update_ruby
+ unless ARGV.named.empty?
+ abort <<-EOS.undent
+ This command updates brew itself, and does not take formula names.
+ Use `brew upgrade <formula>`.
+ EOS
+ end
+
+ # check permissions
+ checks = Diagnostic::Checks.new
+ %w[
+ check_access_usr_local
+ check_access_homebrew_repository
+ ].each do |check|
+ out = checks.send(check)
+ odie out unless out.nil?
+ end
+
+ # ensure git is installed
+ Utils.ensure_git_installed!
+
+ # ensure GIT_CONFIG is unset as we need to operate on .git/config
+ ENV.delete("GIT_CONFIG")
+
+ cd HOMEBREW_REPOSITORY
+ git_init_if_necessary
+
+ # migrate to new directories based tap structure
+ migrate_taps
+
+ report = Report.new
+ master_updater = Updater.new(HOMEBREW_REPOSITORY)
+ master_updater.pull!
+ master_updated = master_updater.updated?
+ if master_updated
+ initial_short = shorten_revision(master_updater.initial_revision)
+ current_short = shorten_revision(master_updater.current_revision)
+ puts "Updated Homebrew from #{initial_short} to #{current_short}."
+ end
+ report.update(master_updater.report)
+
+ # rename Taps directories
+ # this procedure will be removed in the future if it seems unnecessasry
+ rename_taps_dir_if_necessary
+
+ updated_taps = []
+ Tap.each do |tap|
+ next unless tap.git?
+
+ tap.path.cd do
+ updater = Updater.new(tap.path)
+
+ begin
+ updater.pull!
+ rescue
+ onoe "Failed to update tap: #{tap}"
+ else
+ updated_taps << tap.name if updater.updated?
+ report.update(updater.report) do |_key, oldval, newval|
+ oldval.concat(newval)
+ end
+ end
+ end
+ end
+ unless updated_taps.empty?
+ puts "Updated #{updated_taps.size} tap#{plural(updated_taps.size)} " \
+ "(#{updated_taps.join(", ")})."
+ end
+ puts "Already up-to-date." unless master_updated || !updated_taps.empty?
+
+ Tap.clear_cache
+ Tap.each(&:link_manpages)
+
+ # automatically tap any migrated formulae's new tap
+ report.select_formula(:D).each do |f|
+ next unless (dir = HOMEBREW_CELLAR/f).exist?
+ migration = TAP_MIGRATIONS[f]
+ next unless migration
+ tap = Tap.fetch(*migration.split("/"))
+ tap.install unless tap.installed?
+
+ # update tap for each Tab
+ tabs = dir.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
+ next if tabs.first.source["tap"] != "Homebrew/homebrew"
+ tabs.each { |tab| tab.source["tap"] = "#{tap.user}/homebrew-#{tap.repo}" }
+ tabs.each(&:write)
+ end if load_tap_migrations
+
+ load_formula_renames
+ report.update_renamed
+
+ # Migrate installed renamed formulae from core and taps.
+ report.select_formula(:R).each do |oldname, newname|
+ if oldname.include?("/")
+ user, repo, oldname = oldname.split("/", 3)
+ newname = newname.split("/", 3).last
+ else
+ user = "homebrew"
+ repo = "homebrew"
+ end
+
+ next unless (dir = HOMEBREW_CELLAR/oldname).directory? && !dir.subdirs.empty?
+
+ begin
+ f = Formulary.factory("#{user}/#{repo}/#{newname}")
+ # short term fix to prevent situation like https://github.com/Homebrew/homebrew/issues/45616
+ rescue Exception
+ end
+
+ next unless f
+
+ begin
+ migrator = Migrator.new(f)
+ migrator.migrate
+ rescue Migrator::MigratorDifferentTapsError
+ end
+ end
+
+ if report.empty?
+ puts "No changes to formulae." if master_updated || !updated_taps.empty?
+ else
+ report.dump
+ end
+ Descriptions.update_cache(report)
+ end
+
+ private
+
+ def shorten_revision(revision)
+ `git rev-parse --short #{revision}`.chomp
+ end
+
+ def git_init_if_necessary
+ if Dir[".git/*"].empty?
+ safe_system "git", "init"
+ safe_system "git", "config", "core.autocrlf", "false"
+ safe_system "git", "config", "remote.origin.url", "https://github.com/Homebrew/homebrew.git"
+ safe_system "git", "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"
+ safe_system "git", "fetch", "origin"
+ safe_system "git", "reset", "--hard", "origin/master"
+ end
+
+ if `git remote show origin -n` =~ /Fetch URL: \S+mxcl\/homebrew/
+ safe_system "git", "remote", "set-url", "origin", "https://github.com/Homebrew/homebrew.git"
+ safe_system "git", "remote", "set-url", "--delete", "origin", ".*mxcl\/homebrew.*"
+ end
+ rescue Exception
+ FileUtils.rm_rf ".git"
+ raise
+ end
+
+ def rename_taps_dir_if_necessary
+ Dir.glob("#{HOMEBREW_LIBRARY}/Taps/*/") do |tapd|
+ begin
+ if File.directory?(tapd + "/.git")
+ tapd_basename = File.basename(tapd)
+ if tapd_basename.include?("-")
+ # only replace the *last* dash: yes, tap filenames suck
+ user, repo = tapd_basename.reverse.sub("-", "/").reverse.split("/")
+
+ FileUtils.mkdir_p("#{HOMEBREW_LIBRARY}/Taps/#{user.downcase}")
+ FileUtils.mv(tapd, "#{HOMEBREW_LIBRARY}/Taps/#{user.downcase}/homebrew-#{repo.downcase}")
+
+ if tapd_basename.count("-") >= 2
+ opoo "Homebrew changed the structure of Taps like <someuser>/<sometap>. "\
+ + "So you may need to rename #{HOMEBREW_LIBRARY}/Taps/#{user.downcase}/homebrew-#{repo.downcase} manually."
+ end
+ else
+ opoo "Homebrew changed the structure of Taps like <someuser>/<sometap>. "\
+ "#{tapd} is incorrect name format. You may need to rename it like <someuser>/<sometap> manually."
+ end
+ end
+ rescue => ex
+ onoe ex.message
+ next # next tap directory
+ end
+ end
+ end
+
+ def load_tap_migrations
+ load "tap_migrations.rb"
+ rescue LoadError
+ false
+ end
+
+ def load_formula_renames
+ load "formula_renames.rb"
+ rescue LoadError
+ false
+ end
+end
+
+class Updater
+ attr_reader :initial_revision, :current_revision, :repository
+
+ def initialize(repository)
+ @repository = repository
+ @stashed = false
+ @quiet_args = []
+ @quiet_args << "--quiet" unless ARGV.verbose?
+ end
+
+ def pull!(options = {})
+ # The upstream repository's default branch may not be master;
+ # check refs/remotes/origin/HEAD to see what the default
+ # origin branch name is, and use that. If not set, fall back to "master".
+ begin
+ @upstream_branch = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`
+ @upstream_branch = @upstream_branch.chomp.sub('refs/remotes/origin/', '')
+ rescue ErrorDuringExecution
+ @upstream_branch = "master"
+ end
+
+ begin
+ @initial_branch = `git symbolic-ref --short HEAD 2>/dev/null`.chomp
+ rescue ErrorDuringExecution
+ @initial_branch = ""
+ end
+
+ unless `git status --untracked-files=all --porcelain 2>/dev/null`.chomp.empty?
+ if ARGV.verbose?
+ puts "Stashing uncommitted changes to #{repository}."
+ system "git", "status", "--short", "--untracked-files=all"
+ end
+ safe_system "git", "-c", "user.email=brew-update@localhost",
+ "-c", "user.name=brew update",
+ "stash", "save", "--include-untracked", *@quiet_args
+ safe_system "git", "reset", "--hard", *@quiet_args
+ @stashed = true
+ end
+
+ # Used for testing purposes, e.g., for testing formula migration after
+ # renaming it in the currently checked-out branch. To test run
+ # "brew update --simulate-from-current-branch"
+ if ARGV.include?("--simulate-from-current-branch")
+ @initial_revision = `git rev-parse -q --verify #{@upstream_branch}`.chomp
+ @current_revision = read_current_revision
+ begin
+ safe_system "git", "merge-base", "--is-ancestor", @initial_revision, @current_revision
+ rescue ErrorDuringExecution
+ odie "Your HEAD is not a descendant of '#{@upstream_branch}'."
+ end
+ return
+ end
+
+ if @initial_branch != @upstream_branch && !@initial_branch.empty?
+ # Recreate and check out `#{upstream_branch}` if unable to fast-forward
+ # it to `origin/#{@upstream_branch}`. Otherwise, just check it out.
+ if system("git", "merge-base", "--is-ancestor", @upstream_branch, "origin/#{@upstream_branch}")
+ safe_system "git", "checkout", "--force", @upstream_branch, *@quiet_args
+ else
+ safe_system "git", "checkout", "--force", "-B", @upstream_branch, "origin/#{@upstream_branch}", *@quiet_args
+ end
+ end
+
+ @initial_revision = read_current_revision
+
+ # ensure we don't munge line endings on checkout
+ safe_system "git", "config", "core.autocrlf", "false"
+
+ args = ["pull"]
+ args << "--ff"
+ args << ((ARGV.include? "--rebase") ? "--rebase" : "--no-rebase")
+ args += @quiet_args
+ args << "origin"
+ # the refspec ensures that the default upstream branch gets updated
+ args << "refs/heads/#{@upstream_branch}:refs/remotes/origin/#{@upstream_branch}"
+
+ reset_on_interrupt { safe_system "git", *args }
+
+ @current_revision = read_current_revision
+
+ if @initial_branch != @upstream_branch && !@initial_branch.empty?
+ safe_system "git", "checkout", @initial_branch, *@quiet_args
+ pop_stash
+ else
+ pop_stash_message
+ end
+ end
+
+ def pop_stash
+ return unless @stashed
+ safe_system "git", "stash", "pop", *@quiet_args
+ if ARGV.verbose?
+ puts "Restoring your stashed changes to #{repository}:"
+ system "git", "status", "--short", "--untracked-files"
+ end
+ @stashed = false
+ end
+
+ def pop_stash_message
+ return unless @stashed
+ puts "To restore the stashed changes to #{repository} run:"
+ puts " `cd #{repository} && git stash pop`"
+ @stashed = false
+ end
+
+ def reset_on_interrupt
+ ignore_interrupts { yield }
+ ensure
+ if $?.signaled? && $?.termsig == 2 # SIGINT
+ safe_system "git", "checkout", @initial_branch unless @initial_branch.empty?
+ safe_system "git", "reset", "--hard", @initial_revision, *@quiet_args
+ if @initial_branch
+ pop_stash
+ else
+ pop_stash_message
+ end
+ end
+ end
+
+ def report
+ map = Hash.new { |h, k| h[k] = [] }
+
+ if initial_revision && initial_revision != current_revision
+ wc_revision = read_current_revision
+
+ diff.each_line do |line|
+ status, *paths = line.split
+ src = paths.first
+ dst = paths.last
+
+ next unless File.extname(dst) == ".rb"
+ next unless paths.any? { |p| File.dirname(p) == formula_directory }
+
+ case status
+ when "A", "D"
+ map[status.to_sym] << repository.join(src)
+ when "M"
+ file = repository.join(src)
+ begin
+ formula = Formulary.factory(file)
+ new_version = if wc_revision == current_revision
+ formula.pkg_version
+ else
+ FormulaVersions.new(formula).formula_at_revision(@current_revision, &:pkg_version)
+ end
+ old_version = FormulaVersions.new(formula).formula_at_revision(@initial_revision, &:pkg_version)
+ next if new_version == old_version
+ # short term fix to prevent situation like https://github.com/Homebrew/homebrew/issues/45616
+ rescue Exception => e
+ onoe e if ARGV.homebrew_developer?
+ end
+ map[:M] << file
+ when /^R\d{0,3}/
+ map[:D] << repository.join(src) if File.dirname(src) == formula_directory
+ map[:A] << repository.join(dst) if File.dirname(dst) == formula_directory
+ end
+ end
+ end
+
+ map
+ end
+
+ def updated?
+ initial_revision && initial_revision != current_revision
+ end
+
+ private
+
+ def formula_directory
+ if repository == HOMEBREW_REPOSITORY
+ "Library/Formula"
+ elsif repository.join("Formula").directory?
+ "Formula"
+ elsif repository.join("HomebrewFormula").directory?
+ "HomebrewFormula"
+ else
+ "."
+ end
+ end
+
+ def read_current_revision
+ `git rev-parse -q --verify HEAD`.chomp
+ end
+
+ def diff
+ Utils.popen_read(
+ "git", "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
+ "-M85%", initial_revision, current_revision
+ )
+ end
+
+ def `(cmd)
+ out = super
+ unless $?.success?
+ $stderr.puts(out) unless out.empty?
+ raise ErrorDuringExecution.new(cmd)
+ end
+ ohai(cmd, out) if ARGV.verbose?
+ out
+ end
+end
+
+class Report
+ def initialize
+ @hash = {}
+ end
+
+ def fetch(*args, &block)
+ @hash.fetch(*args, &block)
+ end
+
+ def update(*args, &block)
+ @hash.update(*args, &block)
+ 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
+
+ def update_renamed
+ renamed_formulae = []
+
+ fetch(:D, []).each do |path|
+ case path.to_s
+ when HOMEBREW_TAP_PATH_REGEX
+ oldname = path.basename(".rb").to_s
+ next unless newname = Tap.fetch($1, $2).formula_renames[oldname]
+ else
+ oldname = path.basename(".rb").to_s
+ next unless newname = CoreFormulaRepository.instance.formula_renames[oldname]
+ end
+
+ if fetch(:A, []).include?(newpath = path.dirname.join("#{newname}.rb"))
+ renamed_formulae << [path, newpath]
+ end
+ end
+
+ unless renamed_formulae.empty?
+ @hash[:A] -= renamed_formulae.map(&:last) if @hash[:A]
+ @hash[:D] -= renamed_formulae.map(&:first) if @hash[:D]
+ @hash[:R] = renamed_formulae
+ end
+ end
+
+ def select_formula(key)
+ fetch(key, []).map do |path, newpath|
+ if path.to_s =~ HOMEBREW_TAP_PATH_REGEX
+ tap = Tap.fetch($1, $2)
+ if newpath
+ ["#{tap}/#{path.basename(".rb")}", "#{tap}/#{newpath.basename(".rb")}"]
+ else
+ "#{tap}/#{path.basename(".rb")}"
+ end
+ elsif newpath
+ ["#{path.basename(".rb")}", "#{newpath.basename(".rb")}"]
+ else
+ path.basename(".rb").to_s
+ end
+ end.sort
+ end
+
+ def dump_formula_report(key, title)
+ formula = select_formula(key).map do |name, new_name|
+ # Format list items of renamed formulae
+ if key == :R
+ new_name = pretty_installed(new_name) if installed?(name)
+ "#{name} -> #{new_name}"
+ else
+ installed?(name) ? pretty_installed(name) : name
+ end
+ end
+
+ unless formula.empty?
+ # Dump formula list.
+ ohai title
+ puts_columns(formula)
+ end
+ end
+
+ def installed?(formula)
+ (HOMEBREW_CELLAR/formula.split("/").last).directory?
+ end
+end