diff --git a/Library/Homebrew/cmd/update-bash.sh b/Library/Homebrew/cmd/update-bash.sh new file mode 100755 index 0000000000000000000000000000000000000000..83355202b656759f9688d14c752483ac24134327 --- /dev/null +++ b/Library/Homebrew/cmd/update-bash.sh @@ -0,0 +1,280 @@ +#!/bin/bash + +if [ -z "$HOMEBREW_BREW_FILE" ] +then + echo "Error: $(basename "$0") must be called from brew!" >&2 + exit 1 +fi + +brew() { + "$HOMEBREW_BREW_FILE" "$@" +} + +which_git() { + local which_git + which_git="$(which git 2>/dev/null)" + if [ -n "$which_git" ] && [ "/usr/bin/git" = "$which_git" ] + then + local active_developer_dir + active_developer_dir="$('/usr/bin/xcode-select' -print-path 2>/dev/null)" + if [ -n "$active_developer_dir" ] && [ -x "$active_developer_dir/usr/bin/git" ] + then + which_git="$active_developer_dir/usr/bin/git" + else + which_git="" + fi + fi + echo "$which_git" +} + +git_init_if_necessary() { + if ! [ -d ".git" ] + then + git init -q + git config --bool core.autocrlf false + git config remote.origin.url https://github.com/Homebrew/homebrew.git + git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + fi + + if git remote show origin -n | grep -q "mxcl/homebrew" + then + git remote set-url origin https://github.com/Homebrew/homebrew.git + git remote set-url --delete origin ".*mxcl\/homebrew.*" + fi +} + +repo_var() { + echo "$1" | + sed -e "s|$HOMEBREW_PREFIX||g" \ + -e 's|Library/Taps/||g' \ + -e 's|[^a-z0-9]|_|g' | + tr "[:lower:]" "[:upper:]" +} + +upstream_branch() { + local upstream_branch + upstream_branch="$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | + sed -e 's|refs/remotes/origin/||' )" + [ -z "$upstream_branch" ] && upstream_branch="master" + echo "$upstream_branch" +} + +read_current_revision() { + git rev-parse -q --verify HEAD +} + +# Don't warn about QUIET_ARGS; they need to be unquoted. +# shellcheck disable=SC2086 +pop_stash() { + [ -z "$STASHED" ] && return + git stash pop $QUIET_ARGS + if [ -n "$HOMEBREW_VERBOSE" ] + then + echo "Restoring your stashed changes to $DIR:" + git status --short --untracked-files + fi + unset STASHED +} + +pop_stash_message() { + [ -z "$STASHED" ] && return + echo "To restore the stashed changes to $DIR run:" + echo " 'cd $DIR && git stash pop'" + unset STASHED +} + +# Don't warn about QUIET_ARGS; they need to be unquoted. +# shellcheck disable=SC2086 +reset_on_interrupt() { + [ -z "$INITIAL_BRANCH" ] || git checkout "$INITIAL_BRANCH" + git reset --hard "$INITIAL_REVISION" $QUIET_ARGS + if [ -n "$INITIAL_BRANCH" ] + then + pop_stash + else + pop_stash_message + fi +} + +# Don't warn about QUIET_ARGS; they need to be unquoted. +# shellcheck disable=SC2086 +pull() { + local DIR="$1" + cd "$DIR" || return + TAP_VAR=$(repo_var "$DIR") + unset STASHED + + # 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". + INITIAL_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null)" + UPSTREAM_BRANCH="$(upstream_branch)" + + if [ -n "$(git status --untracked-files=all --porcelain 2>/dev/null)" ] + then + if [ -n "$HOMEBREW_VERBOSE" ] + then + echo "Stashing uncommitted changes to $DIR." + git status --short --untracked-files=all + fi + git -c "user.email=brew-update@localhost" \ + -c "user.name=brew update" \ + stash save --include-untracked $QUIET_ARGS + git reset --hard $QUIET_ARGS + STASHED="1" + fi + + # 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 [ -n "$HOMEBREW_SIMULATE_FROM_CURRENT_BRANCH" ] + then + INITIAL_REVISION="$(git rev-parse -q --verify "$(upstream_branch)")" + CURRENT_REVISION="$(read_current_revision)" + export HOMEBREW_UPDATE_AFTER"$TAP_VAR"="$(git rev-parse "$UPSTREAM_BRANCH")" + if ! git merge-base --is-ancestor "$INITIAL_REVISION" "$CURRENT_REVISION" + then + echo "Your HEAD is not a descendant of $UPSTREAM_BRANCH!" >&2 + exit 1 + fi + return + fi + + if [ "$INITIAL_BRANCH" != "$UPSTREAM_BRANCH" ] && [ -n "$INITIAL_BRANCH" ] + then + # Recreate and check out `#{upstream_branch}` if unable to fast-forward + # it to `origin/#{@upstream_branch}`. Otherwise, just check it out. + if git merge-base --is-ancestor "$UPSTREAM_BRANCH" "origin/$UPSTREAM_BRANCH" &>/dev/null + then + git checkout --force "$UPSTREAM_BRANCH" $QUIET_ARGS + else + git checkout --force -B "$UPSTREAM_BRANCH" "origin/$UPSTREAM_BRANCH" $QUIET_ARGS + fi + fi + + INITIAL_REVISION="$(read_current_revision)" + + # ensure we don't munge line endings on checkout + git config core.autocrlf false + + trap reset_on_interrupt SIGINT + + if [ -n "$HOMEBREW_REBASE" ] + then + git rebase $QUIET_ARGS "origin/$UPSTREAM_BRANCH" + else + git merge --no-edit --ff $QUIET_ARGS "origin/$UPSTREAM_BRANCH" + fi + + trap - SIGINT + + CURRENT_REVISION="$(read_current_revision)" + export HOMEBREW_UPDATE_AFTER"$TAP_VAR"="$(git rev-parse "$UPSTREAM_BRANCH")" + + if [ "$INITIAL_BRANCH" != "$UPSTREAM_BRANCH" ] && [ -n "$INITIAL_BRANCH" ] + then + git checkout "$INITIAL_BRANCH" $QUIET_ARGS + pop_stash + else + pop_stash_message + fi +} + +update-bash() { + if [ -z "$HOMEBREW_DEVELOPER" ] + then + echo "This command is currently only for Homebrew developers' use." >&2 + exit 1 + fi + + for i in "$@" + do + case "$i" in + update|update-bash) shift ;; + --help) brew update --help; exit $? ;; + --verbose) HOMEBREW_VERBOSE=1 ;; + --debug) HOMEBREW_DEBUG=1;; + --rebase) HOMEBREW_REBASE=1 ;; + --simulate-from-current-branch) HOMEBREW_SIMULATE_FROM_CURRENT_BRANCH=1 ;; + --*) ;; + -*v*) HOMEBREW_VERBOSE=1 ;; + -*v*) HOMEBREW_DEBUG=1 ;; + -*) ;; + *) + echo "This command updates brew itself, and does not take formula names." >&2 + echo "Use 'brew upgrade <formula>'." >&2 + exit 1 + ;; + esac + done + + if [ -n "$HOMEBREW_DEBUG" ] + then + set -x + fi + + # check permissions + if [ "$HOMEBREW_PREFIX" = "/usr/local" ] && ! test -w /usr/local + then + echo "Error: /usr/local must be writable!" >&2 + exit 1 + fi + + if ! test -w "$HOMEBREW_REPOSITORY" + then + echo "Error: $HOMEBREW_REPOSITORY must be writable!" >&2 + exit 1 + fi + + if [ -z "$(which_git)" ] + then + brew install git + if [ -z "$(which_git)" ] + then + echo "Error: Git must be installed and in your PATH!" >&2 + exit 1 + fi + fi + + if [ -z "$HOMEBREW_VERBOSE" ] + then + QUIET_ARGS="-q" + fi + + # ensure GIT_CONFIG is unset as we need to operate on .git/config + unset GIT_CONFIG + + cd "$HOMEBREW_REPOSITORY" || { + echo "Error: failed to cd to $HOMEBREW_REPOSITORY!" >&2 + exit 1 + } + git_init_if_necessary + + for DIR in "$HOMEBREW_REPOSITORY" "$HOMEBREW_LIBRARY"/Taps/*/* + do + [ -d "$DIR/.git" ] || continue + cd "$DIR" || continue + TAP_VAR=$(repo_var "$DIR") + export HOMEBREW_UPDATE_BEFORE"$TAP_VAR"="$(git rev-parse "$(upstream_branch)")" + UPSTREAM_BRANCH="$(upstream_branch)" + # the refspec ensures that the default upstream branch gets updated + git fetch $QUIET_ARGS origin \ + "refs/heads/$UPSTREAM_BRANCH:refs/remotes/origin/$UPSTREAM_BRANCH" & + done + + wait + + for DIR in "$HOMEBREW_REPOSITORY" "$HOMEBREW_LIBRARY"/Taps/*/* + do + [ -d "$DIR/.git" ] || continue + pull "$DIR" + done + + cd "$HOMEBREW_REPOSITORY" || { + echo "Error: failed to cd to $HOMEBREW_REPOSITORY!" >&2 + exit 1 + } + + brew update-report "$@" + return $? +} diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb new file mode 100644 index 0000000000000000000000000000000000000000..b40aae4223f78c670d7df0b1bb7f2affe09fea16 --- /dev/null +++ b/Library/Homebrew/cmd/update-report.rb @@ -0,0 +1,351 @@ +require "cmd/tap" +require "formula_versions" +require "migrator" +require "formulary" +require "descriptions" + +module Homebrew + def update_report + unless ENV["HOMEBREW_DEVELOPER"] + odie "This command is currently only for Homebrew developers' use." + end + + # migrate to new directories based tap structure + migrate_taps + + report = Report.new + master_updater = Reporter.new(HOMEBREW_REPOSITORY) + 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| + tap.path.cd do + updater = Reporter.new(tap.path) + updated_taps << tap.name if updater.updated? + report.update(updater.report) do |_key, oldval, newval| + oldval.concat(newval) + 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 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 Reporter + attr_reader :initial_revision, :current_revision, :repository + + def self.repository_variable(repository) + repository.to_s. + gsub("#{HOMEBREW_PREFIX}", ""). + gsub("Library/Taps/", ""). + gsub(/[^a-z0-9]/, "_"). + upcase + end + + def initialize(repository) + @repository = repository + + repo_var = Reporter.repository_variable(@repository) + initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{repo_var}" + @initial_revision = ENV[initial_revision_var].to_s + if @initial_revision.empty? + raise "#{initial_revision_var} is unset!" if ARGV.homebrew_developer? + raise "update-report should not be called directly!" + end + + current_revision_var = "HOMEBREW_UPDATE_AFTER#{repo_var}" + @current_revision = ENV[current_revision_var].to_s + if @current_revision.empty? + raise "#{current_revision_var} is unset!" if ARGV.homebrew_developer? + raise "update-report should not be called directly!" + 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 diff --git a/Library/Homebrew/test/test_update_report.rb b/Library/Homebrew/test/test_update_report.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f3d70afae32c56d699b455a03d74cbc354ed4b7 --- /dev/null +++ b/Library/Homebrew/test/test_update_report.rb @@ -0,0 +1,106 @@ +require "testing_env" +require "cmd/update-report" +require "formula_versions" +require "yaml" + +class ReportTests < Homebrew::TestCase + class ReporterMock < ::Reporter + attr_accessor :diff, :expected, :called + + def initialize(repository) + repo_var = Reporter.repository_variable(repository) + ENV["HOMEBREW_UPDATE_BEFORE#{repo_var}"] = "abcdef12" + ENV["HOMEBREW_UPDATE_AFTER#{repo_var}"] = "abcdef12" + super + @outputs = Hash.new { |h, k| h[k] = [] } + @expected = [] + @called = [] + end + + def in_repo_expect(cmd, output = "") + @expected << cmd + @outputs[cmd] << output + end + + def `(*args) + cmd = args.join(" ") + if @expected.include?(cmd) && !@outputs[cmd].empty? + @called << cmd + @outputs[cmd].shift + else + raise "#{inspect} unexpectedly called backticks: `#{cmd}`" + end + end + alias_method :safe_system, :` + alias_method :system, :` + + def inspect + "#<#{self.class.name}>" + end + end + + def fixture(name) + self.class.fixture_data[name] || "" + end + + def self.fixture_data + @fixture_data ||= YAML.load_file("#{TEST_DIRECTORY}/fixtures/updater_fixture.yaml") + end + + def setup + @updater = ReporterMock.new(HOMEBREW_REPOSITORY) + @report = Report.new + end + + def teardown + FileUtils.rm_rf HOMEBREW_LIBRARY.join("Taps") + end + + def perform_update(fixture_name = "") + Formulary.stubs(:factory).returns(stub(:pkg_version => "1.0")) + FormulaVersions.stubs(:new).returns(stub(:formula_at_revision => "2.0")) + @updater.diff = fixture(fixture_name) + @report.update(@updater.report) + assert_equal @updater.expected, @updater.called + end + + def test_update_homebrew_without_any_changes + perform_update + assert_empty @report + end + + def test_update_homebrew_without_formulae_changes + perform_update("update_git_diff_output_without_formulae_changes") + assert_empty @report.select_formula(:M) + assert_empty @report.select_formula(:A) + assert_empty @report.select_formula(:D) + end + + def test_update_homebrew_with_changed_filetype + perform_update("update_git_diff_output_with_changed_filetype") + end + + def test_update_homebrew_with_restructured_tap + repo = HOMEBREW_LIBRARY.join("Taps", "foo", "bar") + @updater = ReporterMock.new(repo) + repo.join("Formula").mkpath + + perform_update("update_git_diff_output_with_restructured_tap") + end + + def test_update_homebrew_simulate_homebrew_php_restructuring + repo = HOMEBREW_LIBRARY.join("Taps", "foo", "bar") + @updater = ReporterMock.new(repo) + repo.join("Formula").mkpath + + perform_update("update_git_diff_simulate_homebrew_php_restructuring") + end + + def test_update_homebrew_with_tap_formulae_changes + repo = HOMEBREW_LIBRARY.join("Taps", "foo", "bar") + @updater = ReporterMock.new(repo) + repo.join("Formula").mkpath + + perform_update("update_git_diff_output_with_tap_formulae_changes") + end +end