brew/Library/Homebrew/cmd/update.rb
Mike McQuaid 8ea9903a99 update: use git stash silently.
There's been a few issues where users have been confused about these
errors. They may have modified stuff but we probably don't care about
telling them that unless we're debugging other issues.

Closes Homebrew/homebrew#45373.

Signed-off-by: Mike McQuaid <mike@mikemcquaid.com>
2015-10-27 10:45:23 -06:00

473 lines
14 KiB
Ruby

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 <formula>`.
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_user, tap_repo = migration.split "/"
install_tap tap_user, tap_repo
# 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}")
rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS
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 <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 = {})
unless system "git", "diff", "--quiet"
if ARGV.verbose?
puts "Stashing your changes:"
system "git", "status", "--short", "--untracked-files"
end
safe_system "git", "stash", "save", "--include-untracked", *@quiet_args
@stashed = true
end
# 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
# 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", @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 != "master" && !@initial_branch.empty?
safe_system "git", "checkout", @initial_branch, *@quiet_args
end
if @stashed
safe_system "git", "stash", "pop", *@quiet_args
if ARGV.verbose?
puts "Restored your changes:"
system "git", "status", "--short", "--untracked-files"
end
@stashed = false
end
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
safe_system "git", "stash", "pop", *@quiet_args if @stashed
@stashed = false
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
rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS => 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
user = $1
repo = $2.sub("homebrew-", "")
oldname = path.basename(".rb").to_s
next unless newname = Tap.fetch(user, repo).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 = "#{$1}/#{$2.sub("homebrew-", "")}"
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