Class: Tap

Inherits:
Object
  • Object
show all
Extended by:
Cachable, Enumerable
Defined in:
brew/Library/Homebrew/tap.rb

Overview

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 a GitHub repository with the name of user/homebrew-repo. In such case, user/repo will be used as the #name of this Tap, where #user represents GitHub username and #repo represents repository name without leading homebrew-.

Direct Known Subclasses

CoreTap

Constant Summary collapse

TAP_DIRECTORY =
(HOMEBREW_LIBRARY/"Taps").freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Cachable

cache

Instance Attribute Details

#full_nameObject (readonly)

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



70
71
72
# File 'brew/Library/Homebrew/tap.rb', line 70

def full_name
  @full_name
end

#nameObject (readonly)

The name of this Tap. It combines #user and #repo with a slash. #name is always in lowercase. e.g. user/repo



65
66
67
# File 'brew/Library/Homebrew/tap.rb', line 65

def name
  @name
end

#pathObject (readonly)

The local path to this Tap. e.g. /usr/local/Library/Taps/user/homebrew-repo



74
75
76
# File 'brew/Library/Homebrew/tap.rb', line 74

def path
  @path
end

#repoObject (readonly)

The repository name of this Tap without leading homebrew-.



60
61
62
# File 'brew/Library/Homebrew/tap.rb', line 60

def repo
  @repo
end

#userObject (readonly)

The user name of this Tap. Usually, it’s the GitHub username of this Tap’s remote repository.



57
58
59
# File 'brew/Library/Homebrew/tap.rb', line 57

def user
  @user
end

Class Method Details

.cmd_directoriesObject

An array of all tap cmd directory Pathnames



596
597
598
# File 'brew/Library/Homebrew/tap.rb', line 596

def self.cmd_directories
  Pathname.glob TAP_DIRECTORY/"*/*/cmd"
end

.default_cask_tapObject



49
50
51
# File 'brew/Library/Homebrew/tap.rb', line 49

def self.default_cask_tap
  @default_cask_tap ||= fetch("Homebrew", "cask")
end

.eachObject



578
579
580
581
582
583
584
585
586
587
588
# File 'brew/Library/Homebrew/tap.rb', line 578

def self.each
  return unless TAP_DIRECTORY.directory?

  return to_enum unless block_given?

  TAP_DIRECTORY.subdirs.each do |user|
    user.subdirs.each do |repo|
      yield fetch(user.basename.to_s, repo.basename.to_s)
    end
  end
end

.fetch(*args) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'brew/Library/Homebrew/tap.rb', line 18

def self.fetch(*args)
  case args.length
  when 1
    user, repo = args.first.split("/", 2)
  when 2
    user = args.first
    repo = args.second
  end

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

  # 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
  repo = repo.sub(HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX, "")

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

  cache_key = "#{user}/#{repo}".downcase
  cache.fetch(cache_key) { |key| cache[key] = Tap.new(user, repo) }
end

.from_path(path) ⇒ Object



39
40
41
42
43
44
45
46
47
# File 'brew/Library/Homebrew/tap.rb', line 39

def self.from_path(path)
  match = File.expand_path(path).match(HOMEBREW_TAP_PATH_REGEX)
  raise "Invalid tap path '#{path}'" unless match

  fetch(match[:user], match[:repo])
rescue
  # No need to error as a nil tap is sufficient to show failure.
  nil
end

.namesObject

An array of all installed Tap names.



591
592
593
# File 'brew/Library/Homebrew/tap.rb', line 591

def self.names
  map(&:name).sort
end

Instance Method Details

#==(other) ⇒ Object



573
574
575
576
# File 'brew/Library/Homebrew/tap.rb', line 573

def ==(other)
  other = Tap.fetch(other) if other.is_a?(String)
  self.class == other.class && name == other.name
end

#cask_dirObject

Path to the directory of all Cask files for this Tap.



363
364
365
# File 'brew/Library/Homebrew/tap.rb', line 363

def cask_dir
  @cask_dir ||= path/"Casks"
end

#cask_filesObject

An array of all Cask files of this Tap.



395
396
397
398
399
400
401
# File 'brew/Library/Homebrew/tap.rb', line 395

def cask_files
  @cask_files ||= if cask_dir.directory?
    cask_dir.children.select(&method(:ruby_file?))
  else
    []
  end
end

#clear_cacheObject

Clear internal cache



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'brew/Library/Homebrew/tap.rb', line 89

def clear_cache
  @remote = nil
  @repo_var = nil
  @formula_dir = nil
  @cask_dir = nil
  @command_dir = nil
  @formula_files = nil
  @alias_dir = nil
  @alias_files = nil
  @aliases = nil
  @alias_table = nil
  @alias_reverse_table = nil
  @command_files = nil
  @formula_renames = nil
  @tap_migrations = nil
  @config = nil
  remove_instance_variable(:@private) if instance_variable_defined?(:@private)
end

#command_dirObject



475
476
477
# File 'brew/Library/Homebrew/tap.rb', line 475

def command_dir
  @command_dir ||= path/"cmd"
end

#command_file?(file) ⇒ Boolean

Returns:

  • (Boolean)


479
480
481
482
483
484
# File 'brew/Library/Homebrew/tap.rb', line 479

def command_file?(file)
  file = Pathname.new(file) unless file.is_a? Pathname
  file = file.expand_path(path)
  file.parent == command_dir && file.basename.to_s.match?(/^brew(cask)?-/) &&
    (file.executable? || file.extname == ".rb")
end

#command_filesObject

An array of all commands files of this Tap.



487
488
489
490
491
492
493
# File 'brew/Library/Homebrew/tap.rb', line 487

def command_files
  @command_files ||= if command_dir.directory?
    command_dir.children.select(&method(:command_file?))
  else
    []
  end
end

#configObject

TapConfig of this Tap



202
203
204
205
206
207
208
# File 'brew/Library/Homebrew/tap.rb', line 202

def config
  @config ||= begin
    raise TapUnavailableError, name unless installed?

    TapConfig.new(self)
  end
end

#contentsObject



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'brew/Library/Homebrew/tap.rb', line 367

def contents
  contents = []

  if (command_count = command_files.count).positive?
    contents << "#{command_count} #{"command".pluralize(command_count)}"
  end

  if (cask_count = cask_files.count).positive?
    contents << "#{cask_count} #{"cask".pluralize(cask_count)}"
  end

  if (formula_count = formula_files.count).positive?
    contents << "#{formula_count} #{"formula".pluralize(formula_count)}"
  end

  contents
end

#custom_remote?Boolean

True if the #remote of Tap is customized.

Returns:

  • (Boolean)


347
348
349
350
351
# File 'brew/Library/Homebrew/tap.rb', line 347

def custom_remote?
  return true unless remote

  remote.casecmp(default_remote).nonzero?
end

#default_remoteObject

The default remote path to this Tap.



117
118
119
# File 'brew/Library/Homebrew/tap.rb', line 117

def default_remote
  "https://github.com/#{full_name}"
end

#formula_dirObject

Path to the directory of all Formula files for this Tap.



354
355
356
# File 'brew/Library/Homebrew/tap.rb', line 354

def formula_dir
  @formula_dir ||= potential_formula_dirs.find(&:directory?) || path/"Formula"
end

#formula_filesObject

An array of all Formula files of this Tap.



386
387
388
389
390
391
392
# File 'brew/Library/Homebrew/tap.rb', line 386

def formula_files
  @formula_files ||= if formula_dir.directory?
    formula_dir.children.select(&method(:ruby_file?))
  else
    []
  end
end

#formula_namesObject

An array of all Formula names of this Tap.



428
429
430
# File 'brew/Library/Homebrew/tap.rb', line 428

def formula_names
  @formula_names ||= formula_files.map { |f| formula_file_to_name(f) }
end

#formula_renamesObject

Hash with tap formula renames



552
553
554
555
556
557
558
559
560
# File 'brew/Library/Homebrew/tap.rb', line 552

def formula_renames
  require "json"

  @formula_renames ||= if (rename_file = path/"formula_renames.json").file?
    JSON.parse(rename_file.read)
  else
    {}
  end
end

#git?Boolean

True if this Tap is a git repository.

Returns:

  • (Boolean)


129
130
131
# File 'brew/Library/Homebrew/tap.rb', line 129

def git?
  path.git?
end

#git_branchObject

git branch for this Tap.



134
135
136
137
138
# File 'brew/Library/Homebrew/tap.rb', line 134

def git_branch
  raise TapUnavailableError, name unless installed?

  path.git_branch
end

#git_headObject

git HEAD for this Tap.



141
142
143
144
145
# File 'brew/Library/Homebrew/tap.rb', line 141

def git_head
  raise TapUnavailableError, name unless installed?

  path.git_head
end

#git_last_commitObject

Time since git last commit for this Tap.



155
156
157
158
159
# File 'brew/Library/Homebrew/tap.rb', line 155

def git_last_commit
  raise TapUnavailableError, name unless installed?

  path.git_last_commit
end

#git_last_commit_dateObject

git last commit date for this Tap.



162
163
164
165
166
# File 'brew/Library/Homebrew/tap.rb', line 162

def git_last_commit_date
  raise TapUnavailableError, name unless installed?

  path.git_last_commit_date
end

#git_short_headObject

git HEAD in short format for this Tap.



148
149
150
151
152
# File 'brew/Library/Homebrew/tap.rb', line 148

def git_short_head
  raise TapUnavailableError, name unless installed?

  path.git_short_head
end

#install(options = {}) ⇒ Object

Install this Tap.

Parameters:

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :clone_target (String)

    If passed, it will be used as the clone remote.

  • :force_auto_update (Boolean, nil)

    If present, whether to override the logic that skips non-GitHub repositories during auto-updates.

  • :full_clone (Boolean)

    If set as true, full clone will be used.

  • :quiet (Boolean)

    If set, suppress all output.



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
# File 'brew/Library/Homebrew/tap.rb', line 233

def install(options = {})
  require "descriptions"

  full_clone = options.fetch(:full_clone, false)
  quiet = options.fetch(:quiet, false)
  requested_remote = options[:clone_target] || default_remote
  # if :force_auto_update is unset, use nil, meaning "no change"
  force_auto_update = options.fetch(:force_auto_update, nil)

  if official? && DEPRECATED_OFFICIAL_TAPS.include?(repo)
    odie "#{name} was deprecated. This tap is now empty as all its formulae were migrated."
  end

  if installed? && force_auto_update.nil?
    raise TapAlreadyTappedError, name unless full_clone
    raise TapAlreadyUnshallowError, name unless shallow?
  end

  # ensure git is installed
  Utils.ensure_git_installed!

  if installed?
    unless force_auto_update.nil?
      config["forceautoupdate"] = force_auto_update
      return if !full_clone || !shallow?
    end

    if options[:clone_target] && requested_remote != remote
      raise TapRemoteMismatchError.new(name, @remote, requested_remote)
    end

    ohai "Unshallowing #{name}" unless quiet
    args = %w[fetch --unshallow]
    args << "-q" if quiet
    path.cd { safe_system "git", *args }
    return
  end

  clear_cache

  ohai "Tapping #{name}" unless quiet
  args =  %W[clone #{requested_remote} #{path}]
  args << "--depth=1" unless full_clone
  args << "-q" if quiet

  begin
    safe_system "git", *args
    unless Readall.valid_tap?(self, aliases: true)
      raise "Cannot tap #{name}: invalid syntax in tap!" unless ARGV.homebrew_developer?
    end
  rescue Interrupt, RuntimeError
    ignore_interrupts do
      # wait for git to possibly cleanup the top directory when interrupt happens.
      sleep 0.1
      FileUtils.rm_rf path
      path.parent.rmdir_if_possible
    end
    raise
  end

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

  link_completions_and_manpages

  formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")
  puts "Tapped#{formatted_contents} (#{path.abv})." unless quiet
  CacheStoreDatabase.use(:descriptions) do |db|
    DescriptionCacheStore.new(db)
                         .update_from_formula_names!(formula_names)
  end

  return if options[:clone_target]
  return unless private?
  return if quiet

  puts <<~EOS
    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}
      git remote set-url origin git@github.com:#{full_name}.git
  EOS
end

#installed?Boolean

True if this Tap has been installed.

Returns:

  • (Boolean)


211
212
213
# File 'brew/Library/Homebrew/tap.rb', line 211

def installed?
  path.directory?
end

#issues_urlObject

The issues URL of this Tap. e.g. https://github.com/user/homebrew-repo/issues



170
171
172
173
174
# File 'brew/Library/Homebrew/tap.rb', line 170

def issues_url
  return unless official? || !custom_remote?

  "#{default_remote}/issues"
end


317
318
319
320
321
# File 'brew/Library/Homebrew/tap.rb', line 317

def link_completions_and_manpages
  command = "brew tap --repair"
  Utils::Link.link_manpages(path, command)
  Utils::Link.link_completions(path, command)
end

#official?Boolean

True if this Tap is an official Homebrew tap.

Returns:

  • (Boolean)


190
191
192
# File 'brew/Library/Homebrew/tap.rb', line 190

def official?
  user == "Homebrew"
end

#pinObject

Pin this Tap.



509
510
511
512
513
514
515
# File 'brew/Library/Homebrew/tap.rb', line 509

def pin
  raise TapUnavailableError, name unless installed?
  raise TapPinStatusError.new(name, true) if pinned?

  pinned_symlink_path.make_relative_symlink(path)
  @pinned = true
end

#pinned?Boolean

True if this Tap has been pinned.

Returns:

  • (Boolean)


502
503
504
505
506
# File 'brew/Library/Homebrew/tap.rb', line 502

def pinned?
  return @pinned if instance_variable_defined?(:@pinned)

  @pinned = pinned_symlink_path.directory?
end

#potential_formula_dirsObject



358
359
360
# File 'brew/Library/Homebrew/tap.rb', line 358

def potential_formula_dirs
  @potential_formula_dirs ||= [path/"Formula", path/"HomebrewFormula", path].freeze
end

#private?Boolean

True if the remote of this Tap is a private repository.

Returns:

  • (Boolean)


195
196
197
198
199
# File 'brew/Library/Homebrew/tap.rb', line 195

def private?
  return @private if instance_variable_defined?(:@private)

  @private = read_or_set_private_config
end

#remoteObject

The remote path to this Tap. e.g. https://github.com/user/homebrew-repo



110
111
112
113
114
# File 'brew/Library/Homebrew/tap.rb', line 110

def remote
  raise TapUnavailableError, name unless installed?

  @remote ||= path.git_origin
end

#repo_varObject



121
122
123
124
125
126
# File 'brew/Library/Homebrew/tap.rb', line 121

def repo_var
  @repo_var ||= path.to_s
                    .delete_prefix(TAP_DIRECTORY.to_s)
                    .tr("^A-Za-z0-9", "_")
                    .upcase
end

#shallow?Boolean

True if this Tap is not a full clone.

Returns:

  • (Boolean)


216
217
218
# File 'brew/Library/Homebrew/tap.rb', line 216

def shallow?
  (path/".git/shallow").exist?
end

#tap_migrationsObject

Hash with tap migrations



563
564
565
566
567
568
569
570
571
# File 'brew/Library/Homebrew/tap.rb', line 563

def tap_migrations
  require "json"

  @tap_migrations ||= if (migration_file = path/"tap_migrations.json").file?
    JSON.parse(migration_file.read)
  else
    {}
  end
end

#to_hashObject



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'brew/Library/Homebrew/tap.rb', line 528

def to_hash
  hash = {
    "name"          => name,
    "user"          => user,
    "repo"          => repo,
    "path"          => path.to_s,
    "installed"     => installed?,
    "official"      => official?,
    "formula_names" => formula_names,
    "formula_files" => formula_files.map(&:to_s),
    "command_files" => command_files.map(&:to_s),
    "pinned"        => pinned?,
  }

  if installed?
    hash["remote"] = remote
    hash["custom_remote"] = custom_remote?
    hash["private"] = private?
  end

  hash
end

#to_sObject



176
177
178
# File 'brew/Library/Homebrew/tap.rb', line 176

def to_s
  name
end

#uninstallObject

Uninstall this Tap.



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'brew/Library/Homebrew/tap.rb', line 324

def uninstall
  require "descriptions"
  raise TapUnavailableError, name unless installed?

  puts "Untapping #{name}..."

  abv = path.abv
  formatted_contents = contents.presence&.to_sentence&.dup&.prepend(" ")

  unpin if pinned?
  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
  path.parent.rmdir_if_possible
  puts "Untapped#{formatted_contents} (#{abv})."
  clear_cache
end

#unpinObject

Unpin this Tap.



518
519
520
521
522
523
524
525
526
# File 'brew/Library/Homebrew/tap.rb', line 518

def unpin
  raise TapUnavailableError, name unless installed?
  raise TapPinStatusError.new(name, false) unless pinned?

  pinned_symlink_path.delete
  pinned_symlink_path.parent.rmdir_if_possible
  pinned_symlink_path.parent.parent.rmdir_if_possible
  @pinned = false
end

#version_stringObject



180
181
182
183
184
185
186
187
# File 'brew/Library/Homebrew/tap.rb', line 180

def version_string
  return "N/A" unless installed?

  pretty_revision = git_short_head
  return "(no git repository)" unless pretty_revision

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