aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/descriptions.rb
blob: bc198267316aa16e4b706a9a2725c85870d3a5b2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
require "set"
require "formula"
require "formula_versions"

class Descriptions
  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(regex, field = :either)
    ensure_cache

    results = case field
    when :name
      @cache.select { |name, _| name =~ regex }
    when :desc
      @cache.select { |_, desc| desc =~ regex }
    when :either
      @cache.select { |name, desc| (name =~ regex) || (desc =~ regex) }
    end

    new(results)
  end

  # Create an actual instance.
  def initialize(descriptions)
    @descriptions = descriptions
  end

  # Take search results -- a hash mapping formula names to descriptions -- and
  # print them.
  def print
    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
      description = @descriptions[full_name] || blank
      puts "#{Tty.bold}#{printed_name}:#{Tty.reset} #{description}"
    end
  end

  private

  def short_names
    @short_names ||= Hash[@descriptions.keys.map { |k| [k, k.split("/").last] }]
  end

  def short_name_counts
    @short_name_counts ||=
      short_names.values.each_with_object(Hash.new(0)) { |name, counts| counts[name] += 1 }
  end
end