brew/Library/Homebrew/cmd/update.rb
Mike McQuaid fe8a0569ca update: always reset to origin/master.
Do a reset to `origin/master` and then stash but don't pop the stash
after running update (unless you were on a branch). This may be mildly
more annoying for Homebrew developers but means it's easier for our
users who don't understand Git (and particularly when they don't
understand that every tap is a separate Git repository).

Closes Homebrew/homebrew#45825.

Signed-off-by: Mike McQuaid <mike@mikemcquaid.com>
2015-12-07 13:37:27 +00:00

489 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 = 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 <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", "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