From 1c10a6260fb4f4036d68b4c34acc6135b26b93d6 Mon Sep 17 00:00:00 2001
From: Zhiming Wang <zmwangx@gmail.com>
Date: Sun, 25 Dec 2016 18:54:08 -0500
Subject: [PATCH] Hint at new location of migrated formulae

Partial implementation of
https://github.com/Homebrew/brew-evolution/pull/15, along with the ability to
search for deleted formulae in git history (inspired by #1996) which is not
described in the proposal.

See also: #1371.
---
 Library/Homebrew/cmd/info.rb           | 22 ++++++++--
 Library/Homebrew/cmd/install.rb        | 13 ++++++
 Library/Homebrew/exceptions.rb         | 13 ++++++
 Library/Homebrew/historic.rb           | 57 ++++++++++++++++++++++++++
 Library/Homebrew/test/historic_test.rb | 46 +++++++++++++++++++++
 5 files changed, 148 insertions(+), 3 deletions(-)
 create mode 100644 Library/Homebrew/historic.rb
 create mode 100644 Library/Homebrew/test/historic_test.rb

diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb
index b7de0005ca..2d5e34ce2c 100644
--- a/Library/Homebrew/cmd/info.rb
+++ b/Library/Homebrew/cmd/info.rb
@@ -23,6 +23,7 @@ require "formula"
 require "keg"
 require "tab"
 require "json"
+require "historic"
 
 module Homebrew
   module_function
@@ -54,10 +55,25 @@ module Homebrew
           else
             info_formula Formulary.find_with_priority(f)
           end
-        rescue FormulaUnavailableError
+        rescue FormulaUnavailableError => e
           # No formula with this name, try a blacklist lookup
-          raise unless (blacklist = blacklisted?(f))
-          puts blacklist
+          if (blacklist = blacklisted?(f))
+            ofail "#{e.message}\n#{blacklist}"
+          else
+            ofail e.message
+
+            # No point in searching if the specified tap isn't tapped yet
+            next if e.instance_of?(TapFormulaUnavailableError) && !e.tap.installed?
+
+            migrations = search_for_migrated_formula(f)
+            next unless migrations.empty?
+            ohai "Searching among deleted formulae..."
+            begin
+              search_for_deleted_formula(f)
+            rescue
+              nil
+            end
+          end
         end
       end
     end
diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb
index c825e27960..8a99bd3978 100644
--- a/Library/Homebrew/cmd/install.rb
+++ b/Library/Homebrew/cmd/install.rb
@@ -62,6 +62,7 @@ require "formula_installer"
 require "tap"
 require "hardware"
 require "development_tools"
+require "historic"
 
 module Homebrew
   module_function
@@ -212,6 +213,18 @@ module Homebrew
         ofail "What's updog?"
       else
         ofail e.message
+
+        migrations = search_for_migrated_formula(e.name)
+        return unless migrations.empty?
+
+        ohai "Searching among deleted formulae..."
+        begin
+          search_for_deleted_formula(e.name)
+          return
+        rescue
+          nil
+        end
+
         query = query_regexp(e.name)
 
         ohai "Searching for similarly named formulae..."
diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb
index 77da4489e8..e9885890bb 100644
--- a/Library/Homebrew/exceptions.rb
+++ b/Library/Homebrew/exceptions.rb
@@ -94,6 +94,19 @@ class TapFormulaUnavailableError < FormulaUnavailableError
   end
 end
 
+class FormulaExistsError < RuntimeError
+  attr_reader :name, :path
+
+  def initialize(name, path)
+    @name = name
+    @path = path
+  end
+
+  def to_s
+    "Formula #{name} exists in #{path}"
+  end
+end
+
 class FormulaClassUnavailableError < FormulaUnavailableError
   attr_reader :path
   attr_reader :class_name
diff --git a/Library/Homebrew/historic.rb b/Library/Homebrew/historic.rb
new file mode 100644
index 0000000000..0afb9646ab
--- /dev/null
+++ b/Library/Homebrew/historic.rb
@@ -0,0 +1,57 @@
+require "formulary"
+require "tap"
+
+module Homebrew
+  module_function
+
+  # name should not be qualified, since migration of qualified names is already
+  # handled in Formulary::TapLoader.formula_name_path.
+  def search_for_migrated_formula(name, options = {})
+    print_messages = options.fetch(:print_messages, true)
+    migrations = []
+    Tap.each do |old_tap|
+      new_tap_name = old_tap.tap_migrations[name]
+      next unless new_tap_name
+      migrations << [old_tap, new_tap_name]
+      next unless print_messages
+      deprecation = (new_tap_name == "homebrew/boneyard") ? "deprecated " : ""
+      puts "A #{deprecation}formula named \"#{name}\" has been migrated from #{old_tap} to #{new_tap_name}."
+    end
+    migrations
+  end
+
+  # name may be qualified.
+  def search_for_deleted_formula(name, options = {})
+    print_messages = options.fetch(:print_messages, true)
+    warn_shallow = options.fetch(:warn_shallow, false)
+
+    path = Formulary.path name
+    raise FormulaExistsError.new(name, path) if File.exist? path
+    path.to_s =~ HOMEBREW_TAP_PATH_REGEX
+    tap = Tap.new ($1 == "Homebrew" ? "homebrew" : $1), $2.strip_prefix("homebrew-")
+    raise TapUnavailableError, tap.name unless File.exist? tap.path
+    relpath = path.relative_path_from tap.path
+
+    cd tap.path
+
+    if warn_shallow && File.exist?(".git/shallow")
+      opoo <<-EOS.undend
+        The git repository is a shallow clone therefore the output may be incomplete.
+        Use `git fetch -C #{tap.path} --unshallow` to get the full repository.
+      EOS
+    end
+
+    log_cmd = "git log --name-only --max-count=1 --format=$'format:%H\\n%h' -- #{relpath}"
+    hash, hash_abbrev, relpath = Utils.popen_read(log_cmd).lines.map(&:chomp)
+    if hash.to_s.empty? || hash_abbrev.to_s.empty? || relpath.to_s.empty?
+      raise FormulaUnavailableError, name
+    end
+
+    if print_messages
+      puts "#{name} was deleted from #{tap.name} in commit #{hash_abbrev}."
+      puts "Run `brew boneyard #{name}` to show the formula's content prior to its removal."
+    end
+
+    [tap, relpath, hash, hash_abbrev]
+  end
+end
diff --git a/Library/Homebrew/test/historic_test.rb b/Library/Homebrew/test/historic_test.rb
new file mode 100644
index 0000000000..d09656fe0b
--- /dev/null
+++ b/Library/Homebrew/test/historic_test.rb
@@ -0,0 +1,46 @@
+require "testing_env"
+require "historic"
+
+class HistoricTest < Homebrew::TestCase
+  def setup
+    super
+
+    @path = Tap::TAP_DIRECTORY/"homebrew/homebrew-foo"
+    @path.mkpath
+    @tap = Tap.new("Homebrew", "foo")
+
+    (@path/"tap_migrations.json").write <<-EOS.undent
+      { "migrated-formula": "homebrew/bar" }
+    EOS
+    (@path/"Formula/to-delete.rb").write "placeholder"
+
+    @path.cd do
+      shutup do
+        system "git", "init"
+        system "git", "add", "--all"
+        system "git", "commit", "-m", "initial state"
+        system "git", "rm", "Formula/to-delete.rb"
+        system "git", "commit", "-m", "delete formula 'to-delete'"
+      end
+    end
+  end
+
+  def teardown
+    @path.rmtree
+
+    super
+  end
+
+  def test_search_for_migrated_formula
+    migrations = Homebrew.search_for_migrated_formula("migrated-formula", print_messages: false)
+    assert_equal [[@tap, "homebrew/bar"]], migrations
+  end
+
+  def test_search_for_deleted_formula
+    tap, relpath, hash, = Homebrew.search_for_deleted_formula("homebrew/foo/to-delete",
+                                                              print_messages: false)
+    assert_equal tap, @tap
+    assert_equal relpath, "Formula/to-delete.rb"
+    assert_equal `git rev-parse HEAD`.chomp, hash
+  end
+end
-- 
GitLab