
This should help to prevent situations where a user accidentally ends up "stranded" in a branch indefinitely. Additionally, the stash is never popped automatically at the end of a successful update, but the stash-pop message is printed if something is stashed. When an interrupt occurs, the original behavior is still in place (switch back to the old branch and revision, and pop the stash), though this could be changed as well. This commit modifies the behavior of both `brew update` and "update-bash.sh" in the manner described above. The idea for this approach is from Mike McQuaid. Issue Homebrew/homebrew#48812 Closes Homebrew/homebrew#48993. Signed-off-by: Mike McQuaid <mike@mikemcquaid.com>
488 lines
14 KiB
Ruby
488 lines
14 KiB
Ruby
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
|
|
|
|
pop_stash_message
|
|
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
|