Class: Tap

Inherits:
Object
  • Object
show all
Extended by:
Cachable, Enumerable
Defined in:
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

TAP_DIRECTORY =
HOMEBREW_LIBRARY/"Taps"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Cachable

cache

Constructor Details

#initialize(user, repo) ⇒ Tap



74
75
76
77
78
79
80
81
# File 'Library/Homebrew/tap.rb', line 74

def initialize(user, repo)
  @user = user
  @repo = repo
  @name = "#{@user}/#{@repo}".downcase
  @full_name = "#{@user}/homebrew-#{@repo}"
  @path = TAP_DIRECTORY/@full_name.downcase
  @path.extend(GitRepositoryExtension)
end

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`



67
68
69
# File 'Library/Homebrew/tap.rb', line 67

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`



62
63
64
# File 'Library/Homebrew/tap.rb', line 62

def name
  @name
end

#pathObject (readonly)

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



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

def path
  @path
end

#repoObject (readonly)

The repository name of this Tap without leading `homebrew-`.



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

def repo
  @repo
end

#userObject (readonly)

The user name of this Tap. Usually, it's the Github username of this #Tap's remote repository.



54
55
56
# File 'Library/Homebrew/tap.rb', line 54

def user
  @user
end

Class Method Details

.cmd_directoriesObject

an array of all tap cmd directory Pathnames



507
508
509
# File 'Library/Homebrew/tap.rb', line 507

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

.eachObject



489
490
491
492
493
494
495
496
497
498
499
# File 'Library/Homebrew/tap.rb', line 489

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



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

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

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

  # 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.strip_prefix "homebrew-"

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

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

.from_path(path) ⇒ Object



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

def self.from_path(path)
  match = path.to_s.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.



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

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

Instance Method Details

#==(other) ⇒ Object



484
485
486
487
# File 'Library/Homebrew/tap.rb', line 484

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

#alias_dirObject

path to the directory of all alias files for this Tap.



365
366
367
# File 'Library/Homebrew/tap.rb', line 365

def alias_dir
  @alias_dir ||= path/"Aliases"
end

#alias_file_to_name(file) ⇒ Object



517
518
519
# File 'Library/Homebrew/tap.rb', line 517

def alias_file_to_name(file)
  "#{name}/#{file.basename}"
end

#alias_filesObject

an array of all alias files of this Tap.



371
372
373
# File 'Library/Homebrew/tap.rb', line 371

def alias_files
  @alias_files ||= Pathname.glob("#{alias_dir}/*").select(&:file?)
end

#alias_reverse_tableObject

a table mapping formula name to aliases



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

def alias_reverse_table
  return @alias_reverse_table if @alias_reverse_table
  @alias_reverse_table = {}
  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

#alias_tableObject

a table mapping alias to formula name



383
384
385
386
387
388
389
390
# File 'Library/Homebrew/tap.rb', line 383

def alias_table
  return @alias_table if @alias_table
  @alias_table = {}
  alias_files.each do |alias_file|
    @alias_table[alias_file_to_name(alias_file)] = formula_file_to_name(alias_file.resolved_path)
  end
  @alias_table
end

#aliasesObject

an array of all aliases of this Tap.



377
378
379
# File 'Library/Homebrew/tap.rb', line 377

def aliases
  @aliases ||= alias_files.map { |f| alias_file_to_name(f) }
end

#cask_dirObject

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



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

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

#cask_file?(file) ⇒ Boolean

return true if given path would present a Cask file in this Tap. accepts both absolute path and relative path (relative to this Tap's path)



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

def cask_file?(file)
  file = Pathname.new(file) unless file.is_a? Pathname
  file = file.expand_path(path)
  file.extname == ".rb" && file.parent == cask_dir
end

#cask_filesObject

an array of all Cask files of this Tap.



332
333
334
335
336
337
338
# File 'Library/Homebrew/tap.rb', line 332

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

#clear_cacheObject

clear internal cache



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'Library/Homebrew/tap.rb', line 84

def clear_cache
  @remote = nil
  @formula_dir = nil
  @cask_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_filesObject

an array of all commands files of this Tap.



405
406
407
# File 'Library/Homebrew/tap.rb', line 405

def command_files
  @command_files ||= Pathname.glob("#{path}/cmd/brew-*").select(&:executable?)
end

#configObject

TapConfig of this Tap



178
179
180
181
182
183
# File 'Library/Homebrew/tap.rb', line 178

def config
  @config ||= begin
    raise TapUnavailableError, name unless installed?
    TapConfig.new(self)
  end
end

#core_tap?Boolean



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

def core_tap?
  false
end

#custom_remote?Boolean

True if the #remote of Tap is customized.



303
304
305
306
# File 'Library/Homebrew/tap.rb', line 303

def custom_remote?
  return true unless remote
  remote.casecmp(default_remote).nonzero?
end

#default_remoteObject

The default remote path to this Tap.



109
110
111
# File 'Library/Homebrew/tap.rb', line 109

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

#formula_dirObject

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



309
310
311
# File 'Library/Homebrew/tap.rb', line 309

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

#formula_file?(file) ⇒ Boolean

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)



343
344
345
346
347
# File 'Library/Homebrew/tap.rb', line 343

def formula_file?(file)
  file = Pathname.new(file) unless file.is_a? Pathname
  file = file.expand_path(path)
  file.extname == ".rb" && file.parent == formula_dir
end

#formula_file_to_name(file) ⇒ Object



512
513
514
# File 'Library/Homebrew/tap.rb', line 512

def formula_file_to_name(file)
  "#{name}/#{file.basename(".rb")}"
end

#formula_filesObject

an array of all Formula files of this Tap.



323
324
325
326
327
328
329
# File 'Library/Homebrew/tap.rb', line 323

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

#formula_namesObject

an array of all Formula names of this Tap.



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

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

#formula_renamesObject

Hash with tap formula renames



463
464
465
466
467
468
469
470
471
# File 'Library/Homebrew/tap.rb', line 463

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.



114
115
116
# File 'Library/Homebrew/tap.rb', line 114

def git?
  path.git?
end

#git_branchObject

git branch for this Tap.



119
120
121
122
# File 'Library/Homebrew/tap.rb', line 119

def git_branch
  raise TapUnavailableError, name unless installed?
  path.git_branch
end

#git_headObject

git HEAD for this Tap.



125
126
127
128
# File 'Library/Homebrew/tap.rb', line 125

def git_head
  raise TapUnavailableError, name unless installed?
  path.git_head
end

#git_last_commitObject

time since git last commit for this Tap.



137
138
139
140
# File 'Library/Homebrew/tap.rb', line 137

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.



143
144
145
146
# File 'Library/Homebrew/tap.rb', line 143

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.



131
132
133
134
# File 'Library/Homebrew/tap.rb', line 131

def git_short_head
  raise TapUnavailableError, name unless installed?
  path.git_short_head
end

#install(options = {}) ⇒ Object

install this Tap.

Options Hash (options):

  • :clone_targe (String)

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

  • :full_clone (Boolean)

    If set as true, full clone will be used.

  • :quiet (Boolean)

    If set, suppress all output.



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

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 official? && DEPRECATED_OFFICIAL_TAPS.include?(repo)
    odie "#{name} was deprecated. This tap is now empty as all its formulae were migrated."
  end

  if installed?
    raise TapAlreadyTappedError, name unless full_clone
    raise TapAlreadyUnshallowError, name unless shallow?
  end

  # ensure git is installed
  Utils.ensure_git_installed!

  if installed?
    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)
      unless ARGV.homebrew_developer?
        raise "Cannot tap #{name}: invalid syntax in tap!"
      end
    end
  rescue Interrupt, ErrorDuringExecution, 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

  link_completions_and_manpages

  formula_count = formula_files.size
  puts "Tapped #{Formatter.pluralize(formula_count, "formula")} (#{path.abv})" unless quiet
  Descriptions.cache_formulae(formula_names)

  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.



186
187
188
# File 'Library/Homebrew/tap.rb', line 186

def installed?
  path.directory?
end

#issues_urlObject

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



150
151
152
153
# File 'Library/Homebrew/tap.rb', line 150

def issues_url
  return unless official? || !custom_remote?
  "#{default_remote}/issues"
end


279
280
281
282
283
# File 'Library/Homebrew/tap.rb', line 279

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.



167
168
169
# File 'Library/Homebrew/tap.rb', line 167

def official?
  user == "Homebrew"
end

#pinObject

pin this Tap.



422
423
424
425
426
427
# File 'Library/Homebrew/tap.rb', line 422

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.



416
417
418
419
# File 'Library/Homebrew/tap.rb', line 416

def pinned?
  return @pinned if instance_variable_defined?(:@pinned)
  @pinned = pinned_symlink_path.directory?
end

path to the pin record for this Tap.



411
412
413
# File 'Library/Homebrew/tap.rb', line 411

def pinned_symlink_path
  HOMEBREW_LIBRARY/"PinnedTaps/#{name}"
end

#potential_formula_dirsObject



313
314
315
# File 'Library/Homebrew/tap.rb', line 313

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.



172
173
174
175
# File 'Library/Homebrew/tap.rb', line 172

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. `github.com/user/homebrew-repo`



103
104
105
106
# File 'Library/Homebrew/tap.rb', line 103

def remote
  raise TapUnavailableError, name unless installed?
  @remote ||= path.git_origin
end

#shallow?Boolean

True if this Tap is not a full clone.



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

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

#tap_migrationsObject

Hash with tap migrations



474
475
476
477
478
479
480
481
482
# File 'Library/Homebrew/tap.rb', line 474

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



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'Library/Homebrew/tap.rb', line 439

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



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

def to_s
  name
end

#uninstallObject

uninstall this Tap.



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'Library/Homebrew/tap.rb', line 286

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

  puts "Untapping #{name}... (#{path.abv})"
  unpin if pinned?
  formula_count = formula_files.size
  Descriptions.uncache_formulae(formula_names)
  Utils::Link.unlink_manpages(path)
  Utils::Link.unlink_completions(path)
  path.rmtree
  path.parent.rmdir_if_possible
  puts "Untapped #{Formatter.pluralize(formula_count, "formula")}"
  clear_cache
end

#unpinObject

unpin this Tap.



430
431
432
433
434
435
436
437
# File 'Library/Homebrew/tap.rb', line 430

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



159
160
161
162
163
164
# File 'Library/Homebrew/tap.rb', line 159

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