require "cmd/tap" require "cmd/doctor" require "formula_versions" require "migrator" require "formulary" require "descriptions" module Homebrew def update unless ARGV.named.empty? abort <<-EOS.undent This command updates brew itself, and does not take formula names. Use `brew upgrade `. EOS end # check permissions checks = 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 puts "Updated Homebrew from #{master_updater.initial_revision[0, 8]} " \ "to #{master_updater.current_revision[0, 8]}." 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 # 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 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 /. "\ + "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 /. "\ "#{tapd} is incorrect name format. You may need to rename it like / 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", "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? safe_system "git", "checkout", "--force", "-B", @upstream_branch, "origin/#{@upstream_branch}", *@quiet_args 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 = 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) unless formula.empty? # Determine list item indices of installed formulae. formula_installed_index = formula.each_index.select do |index| name, newname = formula[index] installed?(name) || (newname && installed?(newname)) end # Format list items of renamed formulae. if key == :R formula.map! { |oldname, newname| "#{oldname} -> #{newname}" } end # Append suffix '(installed)' to list items of installed formulae. formula_installed_index.each do |index| formula[index] += " (installed)" end # Fetch list items of installed formulae for highlighting. formula_installed = formula.values_at(*formula_installed_index) # Dump formula list. ohai title puts_columns(formula, formula_installed) end end def installed?(formula) (HOMEBREW_CELLAR/formula.split("/").last).directory? end end