409 lines
9.8 KiB
Ruby
Raw Normal View History

require "utils/json"
2015-11-07 16:25:34 +08:00
require "descriptions"
# 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-`.
2015-06-10 15:39:47 +08:00
class Tap
TAP_DIRECTORY = HOMEBREW_LIBRARY/"Taps"
CACHE = {}
def self.clear_cache
CACHE.clear
end
def self.fetch(*args)
case args.length
when 1
user, repo = args.first.split("/", 2)
when 2
user = args[0]
repo = args[1]
end
raise "Invalid tap name" unless user && repo
# we special case homebrew so users don't have to shift in a terminal
user = "Homebrew" if user == "homebrew"
repo = repo.strip_prefix "homebrew-"
if user == "Homebrew" && repo == "homebrew"
return CoreFormulaRepository.instance
end
cache_key = "#{user}/#{repo}".downcase
CACHE.fetch(cache_key) { |key| CACHE[key] = Tap.new(user, repo) }
end
2015-06-10 15:39:47 +08:00
extend Enumerable
# The user name of this {Tap}. Usually, it's the Github username of
# this #{Tap}'s remote repository.
2015-06-10 15:39:47 +08:00
attr_reader :user
# The repository name of this {Tap} without leading `homebrew-`.
2015-06-10 15:39:47 +08:00
attr_reader :repo
# The name of this {Tap}. It combines {#user} and {#repo} with a slash.
# {#name} is always in lowercase.
# e.g. `user/repo`
2015-06-10 15:39:47 +08:00
attr_reader :name
# The local path to this {Tap}.
# e.g. `/usr/local/Library/Taps/user/homebrew-repo`
2015-06-10 15:39:47 +08:00
attr_reader :path
# @private
def initialize(user, repo)
@user = user
2015-06-10 15:39:47 +08:00
@repo = repo
@name = "#{@user}/#{@repo}".downcase
@path = TAP_DIRECTORY/"#{@user}/homebrew-#{@repo}".downcase
end
# The remote path to this {Tap}.
# e.g. `https://github.com/user/homebrew-repo`
def remote
@remote ||= if installed?
if git?
2015-12-06 20:57:28 +08:00
path.cd do
Utils.popen_read("git", "config", "--get", "remote.origin.url").chomp
end
2015-06-10 15:39:47 +08:00
end
else
raise TapUnavailableError, name
2015-06-10 15:39:47 +08:00
end
end
# True if this {Tap} is a git repository.
def git?
2015-12-06 20:57:28 +08:00
(path/".git").exist?
end
2015-06-10 15:39:47 +08:00
def to_s
name
end
# True if this {Tap} is an official Homebrew tap.
2015-06-10 15:39:47 +08:00
def official?
2015-12-06 20:57:28 +08:00
user == "Homebrew"
2015-06-10 15:39:47 +08:00
end
# True if the remote of this {Tap} is a private repository.
2015-06-10 15:39:47 +08:00
def private?
return true if custom_remote?
2015-12-06 20:57:28 +08:00
GitHub.private_repo?(user, "homebrew-#{repo}")
2015-06-10 15:39:47 +08:00
rescue GitHub::HTTPNotFoundError
true
rescue GitHub::Error
false
end
# True if this {Tap} has been installed.
2015-06-10 15:39:47 +08:00
def installed?
2015-12-06 20:57:28 +08:00
path.directory?
2015-06-10 15:39:47 +08:00
end
2015-12-07 14:12:57 +08:00
# @private
def core_formula_repository?
false
end
2015-11-07 16:25:34 +08:00
# install this {Tap}.
#
# @param [Hash] options
# @option options [String] :clone_targe If passed, it will be used as the clone remote.
# @option options [Boolean] :full_clone If set as true, full clone will be used.
def install(options = {})
raise TapAlreadyTappedError, name if installed?
# ensure git is installed
Utils.ensure_git_installed!
ohai "Tapping #{name}"
2015-12-06 20:57:28 +08:00
remote = options[:clone_target] || "https://github.com/#{user}/homebrew-#{repo}"
args = %W[clone #{remote} #{path}]
2015-11-07 16:25:34 +08:00
args << "--depth=1" unless options.fetch(:full_clone, false)
begin
safe_system "git", *args
rescue Interrupt, ErrorDuringExecution
ignore_interrupts do
sleep 0.1 # wait for git to cleanup the top directory when interrupt happens.
2015-12-06 20:57:28 +08:00
path.parent.rmdir_if_possible
2015-11-07 16:25:34 +08:00
end
raise
end
formula_count = formula_files.size
2015-12-06 20:57:28 +08:00
puts "Tapped #{formula_count} formula#{plural(formula_count, "e")} (#{path.abv})"
2015-11-07 16:25:34 +08:00
Descriptions.cache_formulae(formula_names)
if !options[:clone_target] && private?
puts <<-EOS.undent
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:
2015-12-06 20:57:28 +08:00
cd #{path}
git remote set-url origin git@github.com:#{user}/homebrew-#{repo}.git
2015-11-07 16:25:34 +08:00
EOS
end
end
# uninstall this {Tap}.
def uninstall
raise TapUnavailableError, name unless installed?
2015-12-06 20:57:28 +08:00
puts "Untapping #{name}... (#{path.abv})"
2015-11-07 16:25:34 +08:00
unpin if pinned?
formula_count = formula_files.size
Descriptions.uncache_formulae(formula_names)
2015-12-06 20:57:28 +08:00
path.rmtree
path.dirname.rmdir_if_possible
2015-11-07 16:25:34 +08:00
puts "Untapped #{formula_count} formula#{plural(formula_count, "e")}"
end
# True if the {#remote} of {Tap} is customized.
2015-06-10 15:39:47 +08:00
def custom_remote?
return true unless remote
2015-12-06 20:57:28 +08:00
remote.casecmp("https://github.com/#{user}/homebrew-#{repo}") != 0
2015-06-10 15:39:47 +08:00
end
2015-12-06 20:59:31 +08:00
# path to the directory of all {Formula} files for this {Tap}.
def formula_dir
@formula_dir ||= [path/"Formula", path/"HomebrewFormula", path].detect(&:directory?)
end
# an array of all {Formula} files of this {Tap}.
2015-06-10 15:39:47 +08:00
def formula_files
2015-12-06 20:59:31 +08:00
@formula_files ||= if formula_dir
formula_dir.children.select { |p| p.extname == ".rb" }
else
[]
end
2015-06-10 15:39:47 +08:00
end
# an array of all {Formula} names of this {Tap}.
2015-06-10 15:39:47 +08:00
def formula_names
@formula_names ||= formula_files.map { |f| formula_file_to_name(f) }
2015-06-10 15:39:47 +08:00
end
2015-12-06 21:15:43 +08:00
# path to the directory of all alias files for this {Tap}.
# @private
def alias_dir
path/"Aliases"
end
2015-09-12 18:21:22 +08:00
# an array of all alias files of this {Tap}.
# @private
def alias_files
2015-12-06 21:15:43 +08:00
@alias_files ||= Pathname.glob("#{alias_dir}/*").select(&:file?)
2015-09-12 18:21:22 +08:00
end
# an array of all aliases of this {Tap}.
# @private
def aliases
@aliases ||= alias_files.map { |f| alias_file_to_name(f) }
2015-09-12 18:21:22 +08:00
end
# a table mapping alias to formula name
# @private
def alias_table
return @alias_table if @alias_table
@alias_table = Hash.new
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
# a table mapping formula name to aliases
# @private
def alias_reverse_table
return @alias_reverse_table if @alias_reverse_table
@alias_reverse_table = Hash.new
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
# an array of all commands files of this {Tap}.
2015-06-10 15:39:47 +08:00
def command_files
@command_files ||= Pathname.glob("#{path}/cmd/brew-*").select(&:executable?)
2015-06-10 15:39:47 +08:00
end
2015-11-07 16:00:45 +08:00
# path to the pin record for this {Tap}.
# @private
2015-08-09 22:42:46 +08:00
def pinned_symlink_path
2015-12-06 20:57:28 +08:00
HOMEBREW_LIBRARY/"PinnedTaps/#{name}"
2015-08-09 22:42:46 +08:00
end
2015-11-07 16:00:45 +08:00
# True if this {Tap} has been pinned.
2015-08-09 22:42:46 +08:00
def pinned?
return @pinned if instance_variable_defined?(:@pinned)
@pinned = pinned_symlink_path.directory?
2015-08-09 22:42:46 +08:00
end
2015-11-07 16:00:45 +08:00
# pin this {Tap}.
2015-08-09 22:42:46 +08:00
def pin
raise TapUnavailableError, name unless installed?
raise TapPinStatusError.new(name, true) if pinned?
2015-12-06 20:57:28 +08:00
pinned_symlink_path.make_relative_symlink(path)
@pinned = true
2015-08-09 22:42:46 +08:00
end
2015-11-07 16:00:45 +08:00
# unpin this {Tap}.
2015-08-09 22:42:46 +08:00
def unpin
raise TapUnavailableError, name unless installed?
raise TapPinStatusError.new(name, false) unless pinned?
pinned_symlink_path.delete
pinned_symlink_path.dirname.rmdir_if_possible
@pinned = false
2015-08-09 22:42:46 +08:00
end
2015-06-10 15:39:47 +08:00
def to_hash
hash = {
2015-12-06 20:57:28 +08:00
"name" => name,
"user" => user,
"repo" => repo,
"path" => path.to_s,
2015-06-10 15:39:47 +08:00
"installed" => installed?,
"official" => official?,
"formula_names" => formula_names,
"formula_files" => formula_files.map(&:to_s),
2015-08-09 22:42:46 +08:00
"command_files" => command_files.map(&:to_s),
"pinned" => pinned?
2015-06-10 15:39:47 +08:00
}
if installed?
hash["remote"] = remote
hash["custom_remote"] = custom_remote?
end
hash
2015-06-10 15:39:47 +08:00
end
# Hash with tap formula renames
def formula_renames
@formula_renames ||= if (rename_file = path/"formula_renames.json").file?
Utils::JSON.load(rename_file.read)
else
{}
end
end
2015-12-06 22:21:27 +08:00
def ==(other)
other = Tap.fetch(other) if other.is_a?(String)
self.class == other.class && self.name == other.name
end
2015-06-10 15:39:47 +08:00
def self.each
return unless TAP_DIRECTORY.directory?
TAP_DIRECTORY.subdirs.each do |user|
user.subdirs.each do |repo|
yield fetch(user.basename.to_s, repo.basename.to_s)
2015-06-10 15:39:47 +08:00
end
end
end
# an array of all installed {Tap} names.
2015-06-10 15:39:47 +08:00
def self.names
map(&:name)
end
private
def formula_file_to_name(file)
"#{name}/#{file.basename(".rb")}"
end
def alias_file_to_name(file)
"#{name}/#{file.basename}"
end
2015-06-10 15:39:47 +08:00
end
# A specialized {Tap} class to mimic the core formula file system, which shares many
# similarities with normal {Tap}.
# TODO Separate core formulae with core codes. See discussion below for future plan:
# https://github.com/Homebrew/homebrew/pull/46735#discussion_r46820565
class CoreFormulaRepository < Tap
# @private
def initialize
@user = "Homebrew"
@repo = "homebrew"
@name = "Homebrew/homebrew"
@path = HOMEBREW_REPOSITORY
end
def self.instance
@instance ||= CoreFormulaRepository.new
end
# @private
def uninstall
raise "Tap#uninstall is not available for CoreFormulaRepository"
end
# @private
def pin
raise "Tap#pin is not available for CoreFormulaRepository"
end
# @private
def unpin
raise "Tap#unpin is not available for CoreFormulaRepository"
end
# @private
def pinned?
false
end
# @private
def command_files
[]
end
# @private
def custom_remote?
remote != "https://github.com/#{user}/#{repo}.git"
end
2015-12-07 14:12:57 +08:00
# @private
def core_formula_repository?
true
end
# @private
def formula_dir
HOMEBREW_LIBRARY/"Formula"
end
# @private
def alias_dir
HOMEBREW_LIBRARY/"Aliases"
end
# @private
def formula_renames
require "formula_renames"
FORMULA_RENAMES
end
private
def formula_file_to_name(file)
file.basename(".rb").to_s
end
def alias_file_to_name(file)
file.basename.to_s
end
end