diff --git a/Library/Homebrew/cache_store.rb b/Library/Homebrew/cache_store.rb index 8f54c33ad4..14e90fca6a 100644 --- a/Library/Homebrew/cache_store.rb +++ b/Library/Homebrew/cache_store.rb @@ -49,6 +49,28 @@ class CacheStoreDatabase cache_path.exist? end + # Returns the modification time of the cache file (if it already exists). + # + # @return [Time] + def mtime + return unless created? + cache_path.mtime + end + + # Performs a `select` on the underlying database. + # + # @return [Array] + def select(&block) + db.select(&block) + end + + # Returns `true` if the cache is empty. + # + # @return [Boolean] + def empty? + db.empty? + end + private # Lazily loaded database in read/write mode. If this method is called, a @@ -105,14 +127,14 @@ class CacheStore # stored # # @abstract - def fetch_type(*) + def fetch(*) raise NotImplementedError end # Deletes data from the cache based on a condition defined in a concrete class # # @abstract - def flush_cache! + def delete!(*) raise NotImplementedError end diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index d670a758d2..53f3ee3a2c 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -161,7 +161,7 @@ module Homebrew cleanup_portable_ruby return if dry_run? - cleanup_linkage_db + cleanup_old_cache_db rm_ds_store else args.each do |arg| @@ -325,8 +325,12 @@ module Homebrew end end - def cleanup_linkage_db - FileUtils.rm_rf [cache/"linkage.db", cache/"linkage.db.db"] + def cleanup_old_cache_db + FileUtils.rm_rf [ + cache/"desc_cache.json", + cache/"linkage.db", + cache/"linkage.db.db", + ] end def rm_ds_store(dirs = nil) diff --git a/Library/Homebrew/cmd/desc.rb b/Library/Homebrew/cmd/desc.rb index a576f7f1a4..ce47714cfa 100644 --- a/Library/Homebrew/cmd/desc.rb +++ b/Library/Homebrew/cmd/desc.rb @@ -10,6 +10,7 @@ require "descriptions" require "search" +require "description_cache_store" module Homebrew module_function @@ -21,23 +22,27 @@ module Homebrew search_type << :either if ARGV.flag? "--search" search_type << :name if ARGV.flag? "--name" search_type << :desc if ARGV.flag? "--description" + if search_type.size > 1 + odie "Pick one, and only one, of -s/--search, -n/--name, or -d/--description." + elsif search_type.present? && ARGV.named.empty? + odie "You must provide a search term." + end - if search_type.empty? + results = if search_type.empty? raise FormulaUnspecifiedError if ARGV.named.empty? desc = {} ARGV.formulae.each { |f| desc[f.full_name] = f.desc } - results = Descriptions.new(desc) - results.print - elsif search_type.size > 1 - odie "Pick one, and only one, of -s/--search, -n/--name, or -d/--description." - elsif !ARGV.named.empty? + Descriptions.new(desc) + else arg = ARGV.named.join(" ") string_or_regex = query_regexp(arg) - results = Descriptions.search(string_or_regex, search_type.first) - results.print - else - odie "You must provide a search term." + CacheStoreDatabase.use(:descriptions) do |db| + cache_store = DescriptionCacheStore.new(db) + Descriptions.search(string_or_regex, search_type.first, cache_store) + end end + + results.print end end diff --git a/Library/Homebrew/cmd/update-report.rb b/Library/Homebrew/cmd/update-report.rb index 36f64cb62e..86fbff79df 100644 --- a/Library/Homebrew/cmd/update-report.rb +++ b/Library/Homebrew/cmd/update-report.rb @@ -8,6 +8,7 @@ require "formulary" require "descriptions" require "cleanup" require "update_migrator" +require "description_cache_store" module Homebrew module_function @@ -127,7 +128,10 @@ module Homebrew hub.dump hub.reporters.each(&:migrate_tap_migration) hub.reporters.each(&:migrate_formula_rename) - Descriptions.update_cache(hub) + CacheStoreDatabase.use(:descriptions) do |db| + DescriptionCacheStore.new(db) + .update_from_report!(hub) + end end puts if ARGV.include?("--preinstall") end diff --git a/Library/Homebrew/description_cache_store.rb b/Library/Homebrew/description_cache_store.rb new file mode 100644 index 0000000000..0cd0e7a881 --- /dev/null +++ b/Library/Homebrew/description_cache_store.rb @@ -0,0 +1,88 @@ +require "set" +require "cache_store" +require "searchable" + +# +# `DescriptionCacheStore` provides methods to fetch and mutate linkage-specific data used +# by the `brew linkage` command +# +class DescriptionCacheStore < CacheStore + include Searchable + + # Inserts a formula description into the cache if it does not exist or + # updates the formula description if it does exist + # + # @param [String] formula_name: the name of the formula to set + # @param [String] description: the description from the formula to set + # @return [nil] + def update!(formula_name, description) + database.set(formula_name, description) + end + + # Delete the formula description from the `DescriptionCacheStore` + # + # @param [String] formula_name: the name of the formula to delete + # @return [nil] + def delete!(formula_name) + database.delete(formula_name) + end + + # If the database is empty `update!` it with all known formulae. + # @return [nil] + def populate_if_empty! + return unless database.empty? + Formula.each { |f| update!(f.full_name, f.desc) } + end + + # Use an update report to update the `DescriptionCacheStore`. + # + # @param [Report] report: an update report generated by cmd/update.rb + # @return [nil] + def update_from_report!(report) + return if report.empty? + + renamings = report.select_formula(:R) + alterations = report.select_formula(:A) + + report.select_formula(:M) + + renamings.map(&:last) + + update_from_formula_names!(alterations) + delete_from_formula_names!(report.select_formula(:D) + + renamings.map(&:first)) + end + + # Use an array of formulae names to update the `DescriptionCacheStore`. + # + # @param [Array] formula_names: the formulae to update. + # @return [nil] + def update_from_formula_names!(formula_names) + formula_names.each do |name| + begin + update!(name, Formula[name].desc) + rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS => e + p e + delete!(name) + end + end + end + + # Use an array of formulae names to delete them from the `DescriptionCacheStore`. + # + # @param [Array] formula_names: the formulae to delete. + # @return [nil] + def delete_from_formula_names!(formula_names) + formula_names.each(&method(:delete!)) + end + + private + + # Not implemented; access is through `Searchable`. + def fetch + super + end + + # `select` from the underlying database. + def select(&block) + database.select(&block) + end +end diff --git a/Library/Homebrew/descriptions.rb b/Library/Homebrew/descriptions.rb index 96b7fe6982..137c79bf31 100644 --- a/Library/Homebrew/descriptions.rb +++ b/Library/Homebrew/descriptions.rb @@ -6,113 +6,17 @@ require "searchable" class Descriptions extend Homebrew::Search - CACHE_FILE = HOMEBREW_CACHE + "desc_cache.json" - - def self.cache - @cache || load_cache - end - - # If the cache file exists, load it into, and return, a hash; otherwise, - # return nil. - def self.load_cache - @cache = JSON.parse(CACHE_FILE.read) if CACHE_FILE.exist? - end - - # Write the cache to disk after ensuring the existence of the containing - # directory. - def self.save_cache - HOMEBREW_CACHE.mkpath - CACHE_FILE.atomic_write JSON.dump(@cache) - end - - # Create a hash mapping all formulae to their descriptions; - # save it for future use. - def self.generate_cache - @cache = {} - Formula.each do |f| - @cache[f.full_name] = f.desc - end - save_cache - end - - # Return true if the cache exists, and none of the Taps - # repos were updated more recently than it was. - def self.cache_fresh? - return false unless CACHE_FILE.exist? - - cache_mtime = File.mtime(CACHE_FILE) - - Tap.each do |tap| - next unless tap.git? - - repo_mtime = File.mtime(tap.path/".git/refs/heads/master") - return false if repo_mtime > cache_mtime - end - - true - end - - # Create the cache if it doesn't already exist. - def self.ensure_cache - generate_cache unless cache_fresh? && cache - end - - # Take a {Report}, as generated by cmd/update.rb. - # Unless the cache file exists, do nothing. - # If it does exist, but the Report is empty, just touch the cache file. - # Otherwise, use the report to update the cache. - def self.update_cache(report) - return unless CACHE_FILE.exist? - - if report.empty? - FileUtils.touch CACHE_FILE - else - renamings = report.select_formula(:R) - alterations = report.select_formula(:A) + report.select_formula(:M) + - renamings.map(&:last) - cache_formulae(alterations, save: false) - uncache_formulae(report.select_formula(:D) + - renamings.map(&:first)) - end - end - - # Given an array of formula names, add them and their descriptions to the - # cache. Save the updated cache to disk, unless explicitly told not to. - def self.cache_formulae(formula_names, options = { save: true }) - return unless cache - - formula_names.each do |name| - begin - @cache[name] = Formulary.factory(name).desc - rescue FormulaUnavailableError, *FormulaVersions::IGNORED_EXCEPTIONS - @cache.delete(name) - end - end - save_cache if options[:save] - end - - # Given an array of formula names, remove them and their descriptions from - # the cache. Save the updated cache to disk, unless explicitly told not to. - def self.uncache_formulae(formula_names, options = { save: true }) - return unless cache - - formula_names.each { |name| @cache.delete(name) } - save_cache if options[:save] - end - # Given a regex, find all formulae whose specified fields contain a match. - def self.search(string_or_regex, field = :either) - ensure_cache - - @cache.extend(Searchable) + def self.search(string_or_regex, field, cache_store) + cache_store.populate_if_empty! results = case field when :name - @cache.search(string_or_regex) { |name, _| name } + cache_store.search(string_or_regex) { |name, _| name } when :desc - @cache.search(string_or_regex) { |_, desc| desc } + cache_store.search(string_or_regex) { |_, desc| desc } when :either - @cache.search(string_or_regex) + cache_store.search(string_or_regex) end new(results) @@ -129,7 +33,11 @@ class Descriptions blank = Formatter.warning("[no description]") @descriptions.keys.sort.each do |full_name| short_name = short_names[full_name] - printed_name = (short_name_counts[short_name] == 1) ? short_name : full_name + printed_name = if short_name_counts[short_name] == 1 + short_name + else + full_name + end description = @descriptions[full_name] || blank puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}" end @@ -143,6 +51,9 @@ class Descriptions def short_name_counts @short_name_counts ||= - short_names.values.each_with_object(Hash.new(0)) { |name, counts| counts[name] += 1 } + short_names.values + .each_with_object(Hash.new(0)) do |name, counts| + counts[name] += 1 + end end end diff --git a/Library/Homebrew/keg.rb b/Library/Homebrew/keg.rb index 31513d243a..1ed943c58c 100644 --- a/Library/Homebrew/keg.rb +++ b/Library/Homebrew/keg.rb @@ -303,7 +303,7 @@ class Keg CacheStoreDatabase.use(:linkage) do |db| break unless db.created? - LinkageCacheStore.new(path, db).flush_cache! + LinkageCacheStore.new(path, db).delete! end path.rmtree diff --git a/Library/Homebrew/linkage_cache_store.rb b/Library/Homebrew/linkage_cache_store.rb index d4b506e617..b4b6dccfcf 100644 --- a/Library/Homebrew/linkage_cache_store.rb +++ b/Library/Homebrew/linkage_cache_store.rb @@ -41,7 +41,7 @@ class LinkageCacheStore < CacheStore # @param [Symbol] the type to fetch from the `LinkageCacheStore` # @raise [TypeError] error if the type is not in `HASH_LINKAGE_TYPES` # @return [Hash] - def fetch_type(type) + def fetch(type) unless HASH_LINKAGE_TYPES.include?(type) raise TypeError, <<~EOS Can't fetch types that are not defined for the linkage store @@ -53,8 +53,10 @@ class LinkageCacheStore < CacheStore fetch_hash_values(type) end + # Delete the keg from the `LinkageCacheStore` + # # @return [nil] - def flush_cache! + def delete! database.delete(@keg_path) end diff --git a/Library/Homebrew/linkage_checker.rb b/Library/Homebrew/linkage_checker.rb index e3bc950bd2..9e1a34fe69 100644 --- a/Library/Homebrew/linkage_checker.rb +++ b/Library/Homebrew/linkage_checker.rb @@ -79,9 +79,9 @@ class LinkageChecker keg_files_dylibs = nil if rebuild_cache - store&.flush_cache! + store&.delete! else - keg_files_dylibs = store&.fetch_type(:keg_files_dylibs) + keg_files_dylibs = store&.fetch(:keg_files_dylibs) end keg_files_dylibs_was_empty = false diff --git a/Library/Homebrew/search.rb b/Library/Homebrew/search.rb index 6362e3114f..c4da854000 100644 --- a/Library/Homebrew/search.rb +++ b/Library/Homebrew/search.rb @@ -1,4 +1,5 @@ require "searchable" +require "description_cache_store" module Homebrew module Search @@ -14,7 +15,10 @@ module Homebrew def search_descriptions(string_or_regex) ohai "Formulae" - Descriptions.search(string_or_regex, :desc).print + CacheStoreDatabase.use(:descriptions) do |db| + cache_store = DescriptionCacheStore.new(db) + Descriptions.search(string_or_regex, :desc, cache_store).print + end end def search_taps(query, silent: false) diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 5895d96db8..6a17b57330 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -1,5 +1,6 @@ require "extend/cachable" require "readall" +require "description_cache_store" # a {Tap} is used to extend the formulae provided by Homebrew core. # Usually, it's synced with a remote git repository. And it's likely @@ -299,7 +300,10 @@ class Tap formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ") puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet - Descriptions.cache_formulae(formula_names) + CacheStoreDatabase.use(:descriptions) do |db| + DescriptionCacheStore.new(db) + .update_from_formula_names!(formula_names) + end return if options[:clone_target] return unless private? @@ -331,7 +335,10 @@ class Tap formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ") unpin if pinned? - Descriptions.uncache_formulae(formula_names) + CacheStoreDatabase.use(:descriptions) do |db| + DescriptionCacheStore.new(db) + .delete_from_formula_names!(formula_names) + end Utils::Link.unlink_manpages(path) Utils::Link.unlink_completions(path) path.rmtree diff --git a/Library/Homebrew/test/cmd/desc_spec.rb b/Library/Homebrew/test/cmd/desc_spec.rb index dece732672..d4121481ed 100644 --- a/Library/Homebrew/test/cmd/desc_spec.rb +++ b/Library/Homebrew/test/cmd/desc_spec.rb @@ -1,6 +1,4 @@ describe "brew desc", :integration_test do - let(:desc_cache) { HOMEBREW_CACHE/"desc_cache.json" } - it "shows a given Formula's description" do setup_test_formula "testball" @@ -9,14 +7,4 @@ describe "brew desc", :integration_test do .and not_to_output.to_stderr .and be_a_success end - - describe "--description" do - it "creates a description cache" do - expect(desc_cache).not_to exist - - expect { brew "desc", "--description", "testball" }.to be_a_success - - expect(desc_cache).to exist - end - end end diff --git a/Library/Homebrew/test/cmd/search_spec.rb b/Library/Homebrew/test/cmd/search_spec.rb index 1985362ce8..f6abbf50bc 100644 --- a/Library/Homebrew/test/cmd/search_spec.rb +++ b/Library/Homebrew/test/cmd/search_spec.rb @@ -34,7 +34,7 @@ describe "brew search", :integration_test do end describe "--desc" do - let(:desc_cache) { HOMEBREW_CACHE/"desc_cache.json" } + let(:desc_cache) { HOMEBREW_CACHE/"descriptions.json" } it "supports searching in descriptions and creates a description cache" do expect(desc_cache).not_to exist diff --git a/Library/Homebrew/test/description_cache_store_spec.rb b/Library/Homebrew/test/description_cache_store_spec.rb new file mode 100644 index 0000000000..4a3f4da72a --- /dev/null +++ b/Library/Homebrew/test/description_cache_store_spec.rb @@ -0,0 +1,50 @@ +require "description_cache_store" + +describe DescriptionCacheStore do + subject(:cache_store) { described_class.new(database) } + + let(:database) { double("database") } + let(:formula_name) { "test_name" } + let(:description) { "test_description" } + + describe "#update!" do + it "sets the formula description" do + expect(database).to receive(:set).with(formula_name, description) + cache_store.update!(formula_name, description) + end + end + + describe "#delete!" do + it "deletes the formula description" do + expect(database).to receive(:delete).with(formula_name) + cache_store.delete!(formula_name) + end + end + + describe "#update_from_report!" do + let(:report) { double(select_formula: [], empty?: false) } + + it "reads from the report" do + cache_store.update_from_report!(report) + end + end + + describe "#update_from_formula_names!" do + it "sets the formulae descriptions" do + f = formula do + url "url-1" + desc "desc" + end + expect(Formulary).to receive(:factory).with(f.name).and_return(f) + expect(database).to receive(:set).with(f.name, f.desc) + cache_store.update_from_formula_names!([f.name]) + end + end + + describe "#delete_from_formula_names!" do + it "deletes the formulae descriptions" do + expect(database).to receive(:delete).with(formula_name) + cache_store.delete_from_formula_names!([formula_name]) + end + end +end diff --git a/Library/Homebrew/test/linkage_cache_store_spec.rb b/Library/Homebrew/test/linkage_cache_store_spec.rb index 23cfbda71c..11033343d0 100644 --- a/Library/Homebrew/test/linkage_cache_store_spec.rb +++ b/Library/Homebrew/test/linkage_cache_store_spec.rb @@ -43,27 +43,27 @@ describe LinkageCacheStore do end end - describe "#flush_cache!" do + describe "#delete!" do it "calls `delete` on the `database` with `keg_name` as parameter" do expect(database).to receive(:delete).with(keg_name) - subject.flush_cache! + subject.delete! end end - describe "#fetch_type" do + describe "#fetch" do context "`HASH_LINKAGE_TYPES.include?(type)`" do before do expect(database).to receive(:get).with(keg_name).and_return(nil) end it "returns a `Hash` of values" do - expect(subject.fetch_type(:keg_files_dylibs)).to be_an_instance_of(Hash) + expect(subject.fetch(:keg_files_dylibs)).to be_an_instance_of(Hash) end end context "`type` not in `HASH_LINKAGE_TYPES`" do it "raises a `TypeError` if the `type` is not supported" do - expect { subject.fetch_type(:bad_type) }.to raise_error(TypeError) + expect { subject.fetch(:bad_type) }.to raise_error(TypeError) end end end