aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cmd/update-report.rb
blob: 38e6164b4b635d2a8c8f65b0c735153235a44d94 (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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
require "cmd/tap"
require "formula_versions"
require "migrator"
require "formulary"
require "descriptions"

module Homebrew
  def update_report
    # 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)

    updated_taps = []
    Tap.each do |tap|
      next unless tap.git?
      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

    if !master_updated && updated_taps.empty? && !ARGV.verbose?
      puts "Already up-to-date."
    end

    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 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)
    if repository == HOMEBREW_REPOSITORY
      ""
    else
      repository.to_s.
        strip_prefix(Tap::TAP_DIRECTORY.to_s).
        tr("^A-Za-z0-9", "_").
        upcase
    end
  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