tap.rb 19.9 KB
Newer Older
1
# typed: false
2
3
# frozen_string_literal: true

4
require "commands"
5
require "extend/cachable"
6
require "description_cache_store"
Misty De Meo's avatar
Misty De Meo committed
7

8
# A {Tap} is used to extend the formulae provided by Homebrew core.
9
# Usually, it's synced with a remote Git repository. And it's likely
Mike McQuaid's avatar
Mike McQuaid committed
10
# a GitHub repository with the name of `user/homebrew-repo`. In such
11
12
13
# cases, `user/repo` will be used as the {#name} of this {Tap}, where
# {#user} represents the GitHub username and {#repo} represents the repository
# name without the leading `homebrew-`.
Xu Cheng's avatar
Xu Cheng committed
14
class Tap
Markus Reiter's avatar
Markus Reiter committed
15
16
  extend T::Sig

17
  extend Cachable
Xu Cheng's avatar
Xu Cheng committed
18

19
  TAP_DIRECTORY = (HOMEBREW_LIBRARY/"Taps").freeze
Xu Cheng's avatar
Xu Cheng committed
20

21
22
23
  HOMEBREW_TAP_FORMULA_RENAMES_FILE = "formula_renames.json"
  HOMEBREW_TAP_MIGRATIONS_FILE = "tap_migrations.json"
  HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR = "audit_exceptions"
24
  HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS = "pypi_formula_mappings.json"
25
26
27
28
29

  HOMEBREW_TAP_JSON_FILES = %W[
    #{HOMEBREW_TAP_FORMULA_RENAMES_FILE}
    #{HOMEBREW_TAP_MIGRATIONS_FILE}
    #{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*.json
30
    #{HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS}
31
32
  ].freeze

33
34
35
36
37
  def self.fetch(*args)
    case args.length
    when 1
      user, repo = args.first.split("/", 2)
    when 2
38
39
      user = args.first
      repo = args.second
40
41
    end

42
    raise "Invalid tap name '#{args.join("/")}'" if [user, repo].any? { |part| part.nil? || part.include?("/") }
43

Shaun Jackman's avatar
Shaun Jackman committed
44
45
    # We special case homebrew and linuxbrew so that users don't have to shift in a terminal.
    user = user.capitalize if ["homebrew", "linuxbrew"].include? user
46
    repo = repo.sub(HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX, "")
47

48
    return CoreTap.instance if ["Homebrew", "Linuxbrew"].include?(user) && ["core", "homebrew"].include?(repo)
49

Xu Cheng's avatar
Xu Cheng committed
50
    cache_key = "#{user}/#{repo}".downcase
51
    cache.fetch(cache_key) { |key| cache[key] = Tap.new(user, repo) }
Xu Cheng's avatar
Xu Cheng committed
52
53
  end

54
  def self.from_path(path)
55
    match = File.expand_path(path).match(HOMEBREW_TAP_PATH_REGEX)
56
    return if match.blank? || match[:user].blank? || match[:repo].blank?
Markus Reiter's avatar
Markus Reiter committed
57

Markus Reiter's avatar
Markus Reiter committed
58
    fetch(match[:user], match[:repo])
59
60
  end

Markus Reiter's avatar
Markus Reiter committed
61
62
63
64
  def self.default_cask_tap
    @default_cask_tap ||= fetch("Homebrew", "cask")
  end

Xu Cheng's avatar
Xu Cheng committed
65
66
  extend Enumerable

Mike McQuaid's avatar
Mike McQuaid committed
67
  # The user name of this {Tap}. Usually, it's the GitHub username of
68
  # this {Tap}'s remote repository.
Xu Cheng's avatar
Xu Cheng committed
69
  attr_reader :user
Xu Cheng's avatar
Xu Cheng committed
70

71
  # The repository name of this {Tap} without the leading `homebrew-`.
Xu Cheng's avatar
Xu Cheng committed
72
  attr_reader :repo
Xu Cheng's avatar
Xu Cheng committed
73
74
75
76

  # The name of this {Tap}. It combines {#user} and {#repo} with a slash.
  # {#name} is always in lowercase.
  # e.g. `user/repo`
Xu Cheng's avatar
Xu Cheng committed
77
  attr_reader :name
Xu Cheng's avatar
Xu Cheng committed
78

79
80
81
82
83
  # The full name of this {Tap}, including the `homebrew-` prefix.
  # It combines {#user} and 'homebrew-'-prefixed {#repo} with a slash.
  # e.g. `user/homebrew-repo`
  attr_reader :full_name

Xu Cheng's avatar
Xu Cheng committed
84
85
  # The local path to this {Tap}.
  # e.g. `/usr/local/Library/Taps/user/homebrew-repo`
Xu Cheng's avatar
Xu Cheng committed
86
87
  attr_reader :path

88
  # @private
89
  def initialize(user, repo)
90
    @user = user
Xu Cheng's avatar
Xu Cheng committed
91
92
    @repo = repo
    @name = "#{@user}/#{@repo}".downcase
93
94
    @full_name = "#{@user}/homebrew-#{@repo}"
    @path = TAP_DIRECTORY/@full_name.downcase
95
    @path.extend(GitRepositoryExtension)
Mike McQuaid's avatar
Mike McQuaid committed
96
97
    @alias_table = nil
    @alias_reverse_table = nil
98
99
  end

100
  # Clear internal cache.
101
102
  def clear_cache
    @remote = nil
103
    @repo_var = nil
104
    @formula_dir = nil
Anastasia Sulyagina's avatar
Anastasia Sulyagina committed
105
    @cask_dir = nil
106
    @command_dir = nil
107
    @formula_files = nil
108
    @alias_dir = nil
109
110
111
112
113
114
    @alias_files = nil
    @aliases = nil
    @alias_table = nil
    @alias_reverse_table = nil
    @command_files = nil
    @formula_renames = nil
Xu Cheng's avatar
Xu Cheng committed
115
    @tap_migrations = nil
116
    @audit_exceptions = nil
117
    @pypi_formula_mappings = nil
Mike McQuaid's avatar
Mike McQuaid committed
118
119
    @config = nil
    remove_instance_variable(:@private) if instance_variable_defined?(:@private)
120
121
  end

Xu Cheng's avatar
Xu Cheng committed
122
123
  # The remote path to this {Tap}.
  # e.g. `https://github.com/user/homebrew-repo`
124
  def remote
125
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
126

127
    @remote ||= path.git_origin
Xu Cheng's avatar
Xu Cheng committed
128
129
  end

Xu Cheng's avatar
Xu Cheng committed
130
  # The default remote path to this {Tap}.
Markus Reiter's avatar
Markus Reiter committed
131
  sig { returns(String) }
Xu Cheng's avatar
Xu Cheng committed
132
  def default_remote
133
    "https://github.com/#{full_name}"
Xu Cheng's avatar
Xu Cheng committed
134
135
  end

136
137
  def repo_var
    @repo_var ||= path.to_s
138
                      .delete_prefix(TAP_DIRECTORY.to_s)
139
140
141
142
                      .tr("^A-Za-z0-9", "_")
                      .upcase
  end

143
  # True if this {Tap} is a Git repository.
Xu Cheng's avatar
Xu Cheng committed
144
  def git?
145
    path.git?
Xu Cheng's avatar
Xu Cheng committed
146
147
  end

Ben Muschol's avatar
Ben Muschol committed
148
149
150
  # git branch for this {Tap}.
  def git_branch
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
151

Ben Muschol's avatar
Ben Muschol committed
152
153
154
    path.git_branch
  end

155
156
157
  # git HEAD for this {Tap}.
  def git_head
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
158

159
    path.git_head
160
161
162
163
164
  end

  # git HEAD in short format for this {Tap}.
  def git_short_head
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
165

166
    path.git_short_head
167
168
  end

169
  # Time since last git commit for this {Tap}.
170
171
  def git_last_commit
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
172

173
    path.git_last_commit
174
175
  end

176
  # Last git commit date for this {Tap}.
177
178
  def git_last_commit_date
    raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
179

180
    path.git_last_commit_date
181
182
  end

Baptiste Fontaine's avatar
Baptiste Fontaine committed
183
184
  # The issues URL of this {Tap}.
  # e.g. `https://github.com/user/homebrew-repo/issues`
Markus Reiter's avatar
Markus Reiter committed
185
  sig { returns(T.nilable(String)) }
Baptiste Fontaine's avatar
Baptiste Fontaine committed
186
  def issues_url
Markus Reiter's avatar
Markus Reiter committed
187
    return unless official? || !custom_remote?
Markus Reiter's avatar
Markus Reiter committed
188

189
    "#{default_remote}/issues"
Baptiste Fontaine's avatar
Baptiste Fontaine committed
190
191
  end

Xu Cheng's avatar
Xu Cheng committed
192
193
194
195
  def to_s
    name
  end

Markus Reiter's avatar
Markus Reiter committed
196
  sig { returns(String) }
197
198
  def version_string
    return "N/A" unless installed?
Markus Reiter's avatar
Markus Reiter committed
199

200
201
    pretty_revision = git_short_head
    return "(no git repository)" unless pretty_revision
Markus Reiter's avatar
Markus Reiter committed
202

203
204
205
    "(git revision #{pretty_revision}; last commit #{git_last_commit_date})"
  end

Xu Cheng's avatar
Xu Cheng committed
206
  # True if this {Tap} is an official Homebrew tap.
Xu Cheng's avatar
Xu Cheng committed
207
  def official?
Xu Cheng's avatar
Xu Cheng committed
208
    user == "Homebrew"
Xu Cheng's avatar
Xu Cheng committed
209
210
  end

Xu Cheng's avatar
Xu Cheng committed
211
  # True if the remote of this {Tap} is a private repository.
Xu Cheng's avatar
Xu Cheng committed
212
  def private?
Mike McQuaid's avatar
Mike McQuaid committed
213
    return @private if instance_variable_defined?(:@private)
Markus Reiter's avatar
Markus Reiter committed
214

Xu Cheng's avatar
Xu Cheng committed
215
    @private = read_or_set_private_config
Mike McQuaid's avatar
Mike McQuaid committed
216
217
  end

218
  # {TapConfig} of this {Tap}.
Mike McQuaid's avatar
Mike McQuaid committed
219
220
221
  def config
    @config ||= begin
      raise TapUnavailableError, name unless installed?
Markus Reiter's avatar
Markus Reiter committed
222

Mike McQuaid's avatar
Mike McQuaid committed
223
224
      TapConfig.new(self)
    end
Xu Cheng's avatar
Xu Cheng committed
225
226
  end

Xu Cheng's avatar
Xu Cheng committed
227
  # True if this {Tap} has been installed.
Xu Cheng's avatar
Xu Cheng committed
228
  def installed?
Xu Cheng's avatar
Xu Cheng committed
229
    path.directory?
Xu Cheng's avatar
Xu Cheng committed
230
231
  end

232
233
234
235
236
  # True if this {Tap} is not a full clone.
  def shallow?
    (path/".git/shallow").exist?
  end

Xu Cheng's avatar
Xu Cheng committed
237
  # @private
Markus Reiter's avatar
Markus Reiter committed
238
  sig { returns(T::Boolean) }
239
  def core_tap?
Xu Cheng's avatar
Xu Cheng committed
240
241
242
    false
  end

243
  # Install this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
244
  #
Mike McQuaid's avatar
Mike McQuaid committed
245
246
  # @param clone_target [String] If passed, it will be used as the clone remote.
  # @param force_auto_update [Boolean, nil] If present, whether to override the
247
  #   logic that skips non-GitHub repositories during auto-updates.
Mike McQuaid's avatar
Mike McQuaid committed
248
249
250
  # @param full_clone [Boolean] If set as true, full clone will be used. If unset/nil, means "no change".
  # @param quiet [Boolean] If set, suppress all output.
  def install(full_clone: true, quiet: false, clone_target: nil, force_auto_update: nil)
251
    require "descriptions"
Bo Anderson's avatar
Bo Anderson committed
252
    require "readall"
Xu Cheng's avatar
Xu Cheng committed
253

254
    if official? && DEPRECATED_OFFICIAL_TAPS.include?(repo)
255
      odie "#{name} was deprecated. This tap is now empty and all its contents were either deleted or migrated."
Markus Reiter's avatar
Markus Reiter committed
256
    elsif user == "caskroom" || name == "phinze/cask"
257
258
      new_repo = repo == "cask" ? "cask" : "cask-#{repo}"
      odie "#{name} was moved. Tap homebrew/#{new_repo} instead."
259
260
    end

Mike McQuaid's avatar
Mike McQuaid committed
261
    requested_remote = clone_target || default_remote
Mike McQuaid's avatar
Mike McQuaid committed
262

Mike McQuaid's avatar
Mike McQuaid committed
263
264
    if installed?
      raise TapRemoteMismatchError.new(name, @remote, requested_remote) if clone_target && requested_remote != remote
265
      raise TapAlreadyTappedError, name if force_auto_update.nil? && !shallow?
266
    end
Xu Cheng's avatar
Xu Cheng committed
267

Xu Cheng's avatar
Xu Cheng committed
268
    # ensure git is installed
Markus Reiter's avatar
Markus Reiter committed
269
    Utils::Git.ensure_installed!
270
271

    if installed?
272
273
274
275
276
      unless force_auto_update.nil?
        config["forceautoupdate"] = force_auto_update
        return if !full_clone || !shallow?
      end

277
      $stderr.ohai "Unshallowing #{name}" unless quiet
Mike McQuaid's avatar
Mike McQuaid committed
278
      args = %w[fetch --unshallow]
279
      args << "-q" if quiet
280
      path.cd { safe_system "git", *args }
281
282
283
284
285
      return
    end

    clear_cache

286
    $stderr.ohai "Tapping #{name}" unless quiet
287
    args =  %W[clone #{requested_remote} #{path}]
288
    args << "--depth=1" unless full_clone
Xu Cheng's avatar
Xu Cheng committed
289
    args << "-q" if quiet
Xu Cheng's avatar
Xu Cheng committed
290
291

    begin
292
      safe_system "git", *args
Mike McQuaid's avatar
Mike McQuaid committed
293
294
      if !Readall.valid_tap?(self, aliases: true) && !Homebrew::EnvConfig.developer?
        raise "Cannot tap #{name}: invalid syntax in tap!"
295
      end
296
    rescue Interrupt, RuntimeError
Xu Cheng's avatar
Xu Cheng committed
297
      ignore_interrupts do
298
299
300
        # wait for git to possibly cleanup the top directory when interrupt happens.
        sleep 0.1
        FileUtils.rm_rf path
Xu Cheng's avatar
Xu Cheng committed
301
        path.parent.rmdir_if_possible
Xu Cheng's avatar
Xu Cheng committed
302
303
304
305
      end
      raise
    end

306
307
    config["forceautoupdate"] = force_auto_update unless force_auto_update.nil?

308
    Commands.rebuild_commands_completion_list
309
    link_completions_and_manpages
Mike McQuaid's avatar
Mike McQuaid committed
310

311
    formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
312
    $stderr.puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet
313
314
315
316
    CacheStoreDatabase.use(:descriptions) do |db|
      DescriptionCacheStore.new(db)
                           .update_from_formula_names!(formula_names)
    end
Xu Cheng's avatar
Xu Cheng committed
317

Mike McQuaid's avatar
Mike McQuaid committed
318
    return if clone_target
Markus Reiter's avatar
Markus Reiter committed
319
320
    return unless private?
    return if quiet
Markus Reiter's avatar
Markus Reiter committed
321

322
    $stderr.puts <<~EOS
Markus Reiter's avatar
Markus Reiter committed
323
324
325
326
      It looks like you tapped a private repository. To avoid entering your
      credentials each time you update, you can use git HTTP credential
      caching or issue the following command:
        cd #{path}
327
        git remote set-url origin git@github.com:#{full_name}.git
Markus Reiter's avatar
Markus Reiter committed
328
    EOS
Xu Cheng's avatar
Xu Cheng committed
329
330
  end

331
332
333
334
  def link_completions_and_manpages
    command = "brew tap --repair"
    Utils::Link.link_manpages(path, command)
    Utils::Link.link_completions(path, command)
Mike McQuaid's avatar
Mike McQuaid committed
335
336
  end

337
  # Uninstall this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
338
  def uninstall
339
    require "descriptions"
Xu Cheng's avatar
Xu Cheng committed
340
341
    raise TapUnavailableError, name unless installed?

342
    $stderr.puts "Untapping #{name}..."
Markus Reiter's avatar
Markus Reiter committed
343
344

    abv = path.abv
345
    formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
Markus Reiter's avatar
Markus Reiter committed
346

Xu Cheng's avatar
Xu Cheng committed
347
    unpin if pinned?
348
349
350
351
    CacheStoreDatabase.use(:descriptions) do |db|
      DescriptionCacheStore.new(db)
                           .delete_from_formula_names!(formula_names)
    end
352
353
    Utils::Link.unlink_manpages(path)
    Utils::Link.unlink_completions(path)
Xu Cheng's avatar
Xu Cheng committed
354
    path.rmtree
Mike McQuaid's avatar
Mike McQuaid committed
355
    path.parent.rmdir_if_possible
356
    $stderr.puts "Untapped#{formatted_contents} (#{abv})."
357
358

    Commands.rebuild_commands_completion_list
359
    clear_cache
Xu Cheng's avatar
Xu Cheng committed
360
361
  end

Xu Cheng's avatar
Xu Cheng committed
362
  # True if the {#remote} of {Tap} is customized.
Xu Cheng's avatar
Xu Cheng committed
363
  def custom_remote?
364
    return true unless remote
Markus Reiter's avatar
Markus Reiter committed
365

366
    remote.casecmp(default_remote).nonzero?
Xu Cheng's avatar
Xu Cheng committed
367
368
  end

369
  # Path to the directory of all {Formula} files for this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
370
  def formula_dir
Mike McQuaid's avatar
Mike McQuaid committed
371
    @formula_dir ||= potential_formula_dirs.find(&:directory?) || path/"Formula"
372
373
374
375
  end

  def potential_formula_dirs
    @potential_formula_dirs ||= [path/"Formula", path/"HomebrewFormula", path].freeze
Xu Cheng's avatar
Xu Cheng committed
376
377
  end

378
  # Path to the directory of all {Cask} files for this {Tap}.
Anastasia Sulyagina's avatar
Anastasia Sulyagina committed
379
  def cask_dir
380
    @cask_dir ||= path/"Casks"
Anastasia Sulyagina's avatar
Anastasia Sulyagina committed
381
382
  end

Markus Reiter's avatar
Markus Reiter committed
383
384
385
386
  def contents
    contents = []

    if (command_count = command_files.count).positive?
387
      contents << "#{command_count} #{"command".pluralize(command_count)}"
Markus Reiter's avatar
Markus Reiter committed
388
389
390
    end

    if (cask_count = cask_files.count).positive?
391
      contents << "#{cask_count} #{"cask".pluralize(cask_count)}"
Markus Reiter's avatar
Markus Reiter committed
392
393
394
    end

    if (formula_count = formula_files.count).positive?
395
      contents << "#{formula_count} #{"formula".pluralize(formula_count)}"
Markus Reiter's avatar
Markus Reiter committed
396
397
398
399
400
    end

    contents
  end

401
  # An array of all {Formula} files of this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
402
  def formula_files
403
    @formula_files ||= if formula_dir.directory?
Emiel Wiedijk's avatar
Emiel Wiedijk committed
404
      formula_dir.children.select(&method(:ruby_file?))
405
406
407
408
409
    else
      []
    end
  end

410
  # An array of all {Cask} files of this {Tap}.
411
  def cask_files
412
    @cask_files ||= if cask_dir.directory?
Emiel Wiedijk's avatar
Emiel Wiedijk committed
413
      cask_dir.children.select(&method(:ruby_file?))
Xu Cheng's avatar
Xu Cheng committed
414
415
416
    else
      []
    end
Xu Cheng's avatar
Xu Cheng committed
417
418
  end

Emiel Wiedijk's avatar
Emiel Wiedijk committed
419
420
421
422
423
424
  # returns true if the file has a Ruby extension
  # @private
  def ruby_file?(file)
    file.extname == ".rb"
  end

Xu Cheng's avatar
Xu Cheng committed
425
426
427
428
429
430
  # return true if given path would present a {Formula} file in this {Tap}.
  # accepts both absolute path and relative path (relative to this {Tap}'s path)
  # @private
  def formula_file?(file)
    file = Pathname.new(file) unless file.is_a? Pathname
    file = file.expand_path(path)
Emiel Wiedijk's avatar
Emiel Wiedijk committed
431
    ruby_file?(file) && file.parent == formula_dir
Xu Cheng's avatar
Xu Cheng committed
432
433
  end

434
  # return true if given path would present a {Cask} file in this {Tap}.
Anastasia Sulyagina's avatar
Anastasia Sulyagina committed
435
436
437
438
439
  # accepts both absolute path and relative path (relative to this {Tap}'s path)
  # @private
  def cask_file?(file)
    file = Pathname.new(file) unless file.is_a? Pathname
    file = file.expand_path(path)
Emiel Wiedijk's avatar
Emiel Wiedijk committed
440
    ruby_file?(file) && file.parent == cask_dir
Anastasia Sulyagina's avatar
Anastasia Sulyagina committed
441
442
  end

443
  # An array of all {Formula} names of this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
444
  def formula_names
445
446
447
448
449
450
    @formula_names ||= formula_files.map(&method(:formula_file_to_name))
  end

  # An array of all {Cask} tokens of this {Tap}.
  def cask_tokens
    @cask_tokens ||= cask_files.map(&method(:formula_file_to_name))
Xu Cheng's avatar
Xu Cheng committed
451
452
  end

Xu Cheng's avatar
Xu Cheng committed
453
454
455
  # path to the directory of all alias files for this {Tap}.
  # @private
  def alias_dir
456
    @alias_dir ||= path/"Aliases"
Xu Cheng's avatar
Xu Cheng committed
457
458
  end

Xu Cheng's avatar
Xu Cheng committed
459
460
461
  # an array of all alias files of this {Tap}.
  # @private
  def alias_files
Xu Cheng's avatar
Xu Cheng committed
462
    @alias_files ||= Pathname.glob("#{alias_dir}/*").select(&:file?)
Xu Cheng's avatar
Xu Cheng committed
463
464
465
466
467
  end

  # an array of all aliases of this {Tap}.
  # @private
  def aliases
468
    @aliases ||= alias_files.map { |f| alias_file_to_name(f) }
Xu Cheng's avatar
Xu Cheng committed
469
470
  end

471
472
473
474
  # a table mapping alias to formula name
  # @private
  def alias_table
    return @alias_table if @alias_table
Markus Reiter's avatar
Markus Reiter committed
475

476
    @alias_table = {}
477
    alias_files.each do |alias_file|
478
      @alias_table[alias_file_to_name(alias_file)] = formula_file_to_name(alias_file.resolved_path)
479
480
481
482
483
484
485
486
    end
    @alias_table
  end

  # a table mapping formula name to aliases
  # @private
  def alias_reverse_table
    return @alias_reverse_table if @alias_reverse_table
Markus Reiter's avatar
Markus Reiter committed
487

488
    @alias_reverse_table = {}
489
490
491
492
493
494
495
    alias_table.each do |alias_name, formula_name|
      @alias_reverse_table[formula_name] ||= []
      @alias_reverse_table[formula_name] << alias_name
    end
    @alias_reverse_table
  end

496
497
498
499
  def command_dir
    @command_dir ||= path/"cmd"
  end

500
  # An array of all commands files of this {Tap}.
Xu Cheng's avatar
Xu Cheng committed
501
  def command_files
502
    @command_files ||= if command_dir.directory?
503
      Commands.find_commands(command_dir)
504
505
506
    else
      []
    end
Xu Cheng's avatar
Xu Cheng committed
507
508
  end

Xu Cheng's avatar
Xu Cheng committed
509
510
  # path to the pin record for this {Tap}.
  # @private
CNA-Bld's avatar
CNA-Bld committed
511
  def pinned_symlink_path
Xu Cheng's avatar
Xu Cheng committed
512
    HOMEBREW_LIBRARY/"PinnedTaps/#{name}"
CNA-Bld's avatar
CNA-Bld committed
513
514
  end

Xu Cheng's avatar
Xu Cheng committed
515
  # True if this {Tap} has been pinned.
CNA-Bld's avatar
CNA-Bld committed
516
  def pinned?
Xu Cheng's avatar
Xu Cheng committed
517
    return @pinned if instance_variable_defined?(:@pinned)
Markus Reiter's avatar
Markus Reiter committed
518

Xu Cheng's avatar
Xu Cheng committed
519
    @pinned = pinned_symlink_path.directory?
CNA-Bld's avatar
CNA-Bld committed
520
521
  end

Xu Cheng's avatar
Xu Cheng committed
522
  def to_hash
Xu Cheng's avatar
Xu Cheng committed
523
    hash = {
Mike McQuaid's avatar
Mike McQuaid committed
524
525
526
527
528
529
      "name"          => name,
      "user"          => user,
      "repo"          => repo,
      "path"          => path.to_s,
      "installed"     => installed?,
      "official"      => official?,
Xu Cheng's avatar
Xu Cheng committed
530
531
      "formula_names" => formula_names,
      "formula_files" => formula_files.map(&:to_s),
532
      "cask_tokens"   => cask_tokens,
533
      "cask_files"    => cask_files.map(&:to_s),
534
      "command_files" => command_files.map(&:to_s),
Xu Cheng's avatar
Xu Cheng committed
535
    }
Xu Cheng's avatar
Xu Cheng committed
536
537
538
539

    if installed?
      hash["remote"] = remote
      hash["custom_remote"] = custom_remote?
540
      hash["private"] = private?
Xu Cheng's avatar
Xu Cheng committed
541
542
543
    end

    hash
Xu Cheng's avatar
Xu Cheng committed
544
545
  end

546
  # Hash with tap formula renames.
Vlad Shablinsky's avatar
Vlad Shablinsky committed
547
  def formula_renames
548
    @formula_renames ||= if (rename_file = path/HOMEBREW_TAP_FORMULA_RENAMES_FILE).file?
549
      JSON.parse(rename_file.read)
Vlad Shablinsky's avatar
Vlad Shablinsky committed
550
551
552
553
554
    else
      {}
    end
  end

555
  # Hash with tap migrations.
Xu Cheng's avatar
Xu Cheng committed
556
  def tap_migrations
557
    @tap_migrations ||= if (migration_file = path/HOMEBREW_TAP_MIGRATIONS_FILE).file?
558
      JSON.parse(migration_file.read)
Xu Cheng's avatar
Xu Cheng committed
559
560
561
562
563
    else
      {}
    end
  end

564
  # Hash with audit exceptions
565
  sig { returns(Hash) }
566
  def audit_exceptions
567
    @audit_exceptions = read_formula_list_directory "#{HOMEBREW_TAP_AUDIT_EXCEPTIONS_DIR}/*"
568
  end
569

570
571
572
573
  # Hash with pypi formula mappings
  sig { returns(Hash) }
  def pypi_formula_mappings
    @pypi_formula_mappings = read_formula_list path/HOMEBREW_TAP_PYPI_FORMULA_MAPPINGS
574
575
  end

Xu Cheng's avatar
Xu Cheng committed
576
577
  def ==(other)
    other = Tap.fetch(other) if other.is_a?(String)
578
    self.class == other.class && name == other.name
Xu Cheng's avatar
Xu Cheng committed
579
580
  end

581
  def self.each(&block)
Xu Cheng's avatar
Xu Cheng committed
582
583
    return unless TAP_DIRECTORY.directory?

Markus Reiter's avatar
Markus Reiter committed
584
    return to_enum unless block
585

Xu Cheng's avatar
Xu Cheng committed
586
587
    TAP_DIRECTORY.subdirs.each do |user|
      user.subdirs.each do |repo|
588
        block.call fetch(user.basename.to_s, repo.basename.to_s)
Xu Cheng's avatar
Xu Cheng committed
589
590
591
592
      end
    end
  end

593
  # An array of all installed {Tap} names.
Xu Cheng's avatar
Xu Cheng committed
594
  def self.names
595
    map(&:name).sort
Xu Cheng's avatar
Xu Cheng committed
596
  end
597

598
  # An array of all tap cmd directory {Pathname}s.
Markus Reiter's avatar
Markus Reiter committed
599
  sig { returns(T::Array[Pathname]) }
Mike McQuaid's avatar
Mike McQuaid committed
600
601
602
603
  def self.cmd_directories
    Pathname.glob TAP_DIRECTORY/"*/*/cmd"
  end

604
  # @private
605
606
607
608
  def formula_file_to_name(file)
    "#{name}/#{file.basename(".rb")}"
  end

609
  # @private
610
611
612
  def alias_file_to_name(file)
    "#{name}/#{file.basename}"
  end
Xu Cheng's avatar
Xu Cheng committed
613
614
615
616
617
618
619
620
621
622
623
624

  private

  def read_or_set_private_config
    case config["private"]
    when "true" then true
    when "false" then false
    else
      config["private"] = begin
        if custom_remote?
          true
        else
625
          GitHub.private_repo?(full_name)
Xu Cheng's avatar
Xu Cheng committed
626
627
628
629
630
631
632
633
        end
      rescue GitHub::HTTPNotFoundError
        true
      rescue GitHub::Error
        false
      end
    end
  end
634

635
  sig { params(file: Pathname).returns(T.any(T::Array[String], Hash)) }
636
637
638
639
640
641
642
643
644
645
  def read_formula_list(file)
    JSON.parse file.read
  rescue JSON::ParserError
    opoo "#{file} contains invalid JSON"
    {}
  rescue Errno::ENOENT
    {}
  end

  sig { params(directory: String).returns(Hash) }
646
647
648
  def read_formula_list_directory(directory)
    list = {}

649
    Pathname.glob(path/directory).each do |exception_file|
650
      list_name = exception_file.basename.to_s.chomp(".json").to_sym
651
      list_contents = read_formula_list exception_file
652

653
      next if list_contents.blank?
654
655
656
657
658
659

      list[list_name] = list_contents
    end

    list
  end
Xu Cheng's avatar
Xu Cheng committed
660
end
661

662
# A specialized {Tap} class for the core formulae.
663
class CoreTap < Tap
Markus Reiter's avatar
Markus Reiter committed
664
665
  extend T::Sig

666
  # @private
Markus Reiter's avatar
Markus Reiter committed
667
  sig { void }
668
  def initialize
669
    super "Homebrew", "core"
670
671
672
  end

  def self.instance
673
    @instance ||= new
674
675
  end

Alyssa Ross's avatar
Alyssa Ross committed
676
  def self.ensure_installed!
677
    return if instance.installed?
Markus Reiter's avatar
Markus Reiter committed
678

Alyssa Ross's avatar
Alyssa Ross committed
679
    safe_system HOMEBREW_BREW_FILE, "tap", instance.name
680
681
  end

Mike McQuaid's avatar
Mike McQuaid committed
682
  # CoreTap never allows shallow clones (on request from GitHub).
Mike McQuaid's avatar
Mike McQuaid committed
683
  def install(full_clone: true, quiet: false, clone_target: nil, force_auto_update: nil)
Mike McQuaid's avatar
Mike McQuaid committed
684
685
    raise "Shallow clones are not supported for homebrew-core!" unless full_clone

Mike McQuaid's avatar
Mike McQuaid committed
686
    remote = Homebrew::EnvConfig.core_git_remote
687
688
689
    if remote != default_remote
      $stderr.puts "HOMEBREW_CORE_GIT_REMOTE set: using #{remote} for Homebrew/core Git remote URL."
    end
Mike McQuaid's avatar
Mike McQuaid committed
690
    super(full_clone: full_clone, quiet: quiet, clone_target: remote, force_auto_update: force_auto_update)
691
692
  end

693
  # @private
Markus Reiter's avatar
Markus Reiter committed
694
  sig { void }
695
696
697
698
699
  def uninstall
    raise "Tap#uninstall is not available for CoreTap"
  end

  # @private
Markus Reiter's avatar
Markus Reiter committed
700
  sig { void }
701
702
703
704
705
  def pin
    raise "Tap#pin is not available for CoreTap"
  end

  # @private
Markus Reiter's avatar
Markus Reiter committed
706
  sig { void }
707
708
709
710
711
  def unpin
    raise "Tap#unpin is not available for CoreTap"
  end

  # @private
Markus Reiter's avatar
Markus Reiter committed
712
  sig { returns(T::Boolean) }
713
714
715
716
717
  def pinned?
    false
  end

  # @private
Markus Reiter's avatar
Markus Reiter committed
718
  sig { returns(T::Boolean) }
719
720
721
722
723
724
  def core_tap?
    true
  end

  # @private
  def formula_dir
725
726
727
728
    @formula_dir ||= begin
      self.class.ensure_installed!
      super
    end
729
730
731
732
  end

  # @private
  def alias_dir
733
734
735
736
    @alias_dir ||= begin
      self.class.ensure_installed!
      super
    end
737
738
739
740
  end

  # @private
  def formula_renames
741
742
743
744
    @formula_renames ||= begin
      self.class.ensure_installed!
      super
    end
745
746
747
748
  end

  # @private
  def tap_migrations
749
750
751
752
    @tap_migrations ||= begin
      self.class.ensure_installed!
      super
    end
753
754
  end

755
756
757
758
759
760
761
762
  # @private
  def audit_exceptions
    @audit_exceptions ||= begin
      self.class.ensure_installed!
      super
    end
  end

763
764
  def pypi_formula_mappings
    @pypi_formula_mappings ||= begin
765
766
767
768
769
      self.class.ensure_installed!
      super
    end
  end

770
771
772
773
774
775
776
777
778
779
  # @private
  def formula_file_to_name(file)
    file.basename(".rb").to_s
  end

  # @private
  def alias_file_to_name(file)
    file.basename.to_s
  end
end
Mike McQuaid's avatar
Mike McQuaid committed
780

781
# Permanent configuration per {Tap} using `git-config(1)`.
Mike McQuaid's avatar
Mike McQuaid committed
782
783
784
785
786
787
788
789
790
class TapConfig
  attr_reader :tap

  def initialize(tap)
    @tap = tap
  end

  def [](key)
    return unless tap.git?
Markus Reiter's avatar
Markus Reiter committed
791
    return unless Utils::Git.available?
Mike McQuaid's avatar
Mike McQuaid committed
792
793

    tap.path.cd do
794
      Utils.popen_read("git", "config", "--get", "homebrew.#{key}").chomp.presence
Mike McQuaid's avatar
Mike McQuaid committed
795
796
797
798
799
    end
  end

  def []=(key, value)
    return unless tap.git?
Markus Reiter's avatar
Markus Reiter committed
800
    return unless Utils::Git.available?
Mike McQuaid's avatar
Mike McQuaid committed
801
802

    tap.path.cd do
803
      safe_system "git", "config", "--replace-all", "homebrew.#{key}", value.to_s
Mike McQuaid's avatar
Mike McQuaid committed
804
805
806
    end
  end
end
807
808

require "extend/os/tap"