lib/tapioca/commands/annotations.rb in tapioca-0.8.3 vs lib/tapioca/commands/annotations.rb in tapioca-0.9.0
- old
+ new
@@ -1,29 +1,42 @@
# typed: strict
# frozen_string_literal: true
-require "net/http"
-
module Tapioca
module Commands
class Annotations < Command
extend T::Sig
sig do
params(
- central_repo_root_uri: String,
- central_repo_index_path: String
+ central_repo_root_uris: T::Array[String],
+ auth: T.nilable(String),
+ netrc_file: T.nilable(String),
+ central_repo_index_path: String,
+ typed_overrides: T::Hash[String, String]
).void
end
- def initialize(central_repo_root_uri:, central_repo_index_path: CENTRAL_REPO_INDEX_PATH)
+ def initialize(
+ central_repo_root_uris:,
+ auth: nil,
+ netrc_file: nil,
+ central_repo_index_path: CENTRAL_REPO_INDEX_PATH,
+ typed_overrides: {}
+ )
super()
- @central_repo_root_uri = central_repo_root_uri
- @index = T.let(fetch_index, RepoIndex)
+ @central_repo_root_uris = central_repo_root_uris
+ @auth = auth
+ @netrc_file = netrc_file
+ @netrc_info = T.let(nil, T.nilable(Netrc))
+ @tokens = T.let(repo_tokens, T::Hash[String, T.nilable(String)])
+ @indexes = T.let({}, T::Hash[String, RepoIndex])
+ @typed_overrides = typed_overrides
end
sig { override.void }
def execute
+ @indexes = fetch_indexes
project_gems = list_gemfile_gems
remove_expired_annotations(project_gems)
fetch_annotations(project_gems)
end
@@ -58,97 +71,216 @@
remove_file(path)
end
say("\nDone\n\n", :green)
end
- sig { returns(RepoIndex) }
- def fetch_index
- say("Retrieving index from central repository... ", [:blue, :bold])
- content = fetch_file(CENTRAL_REPO_INDEX_PATH)
- exit(1) unless content
+ sig { returns(T::Hash[String, RepoIndex]) }
+ def fetch_indexes
+ multiple_repos = @central_repo_root_uris.size > 1
+ repo_number = 1
+ indexes = T.let({}, T::Hash[String, RepoIndex])
+ @central_repo_root_uris.each do |uri|
+ index = fetch_index(uri, repo_number: multiple_repos ? repo_number : nil)
+ next unless index
+
+ indexes[uri] = index
+ repo_number += 1
+ end
+
+ if indexes.empty?
+ say_error("\nCan't fetch annotations without sources (no index fetched)", :bold, :red)
+ exit(1)
+ end
+
+ indexes
+ end
+
+ sig { params(repo_uri: String, repo_number: T.nilable(Integer)).returns(T.nilable(RepoIndex)) }
+ def fetch_index(repo_uri, repo_number:)
+ say("Retrieving index from central repository#{repo_number ? " ##{repo_number}" : ""}... ", [:blue, :bold])
+ content = fetch_file(repo_uri, CENTRAL_REPO_INDEX_PATH)
+ return nil unless content
+
index = RepoIndex.from_json(content)
say("Done", :green)
index
end
sig { params(gem_names: T::Array[String]).returns(T::Array[String]) }
def fetch_annotations(gem_names)
say("Fetching gem annotations from central repository... ", [:blue, :bold])
- fetchable_gems = gem_names.select { |gem_name| @index.has_gem?(gem_name) }
+ fetchable_gems = T.let(Hash.new { |h, k| h[k] = [] }, T::Hash[String, T::Array[String]])
+ gem_names.each_with_object(fetchable_gems) do |gem_name, hash|
+ @indexes.each { |uri, index| hash[gem_name] << uri if index.has_gem?(gem_name) }
+ end
+
if fetchable_gems.empty?
say(" Nothing to do")
exit(0)
end
say("\n")
- fetched_gems = fetchable_gems.select { |name| fetch_annotation(name) }
+ fetched_gems = fetchable_gems.select { |gem_name, repo_uris| fetch_annotation(repo_uris, gem_name) }
say("\nDone", :green)
- fetched_gems
+ fetched_gems.keys.sort
end
- sig { params(gem_name: String).void }
- def fetch_annotation(gem_name)
- content = fetch_file("#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
+ sig { params(repo_uris: T::Array[String], gem_name: String).void }
+ def fetch_annotation(repo_uris, gem_name)
+ contents = repo_uris.map do |repo_uri|
+ fetch_file(repo_uri, "#{CENTRAL_REPO_ANNOTATIONS_DIR}/#{gem_name}.rbi")
+ end
+
+ content = merge_files(gem_name, contents.compact)
return unless content
+ content = apply_typed_override(gem_name, content)
content = add_header(gem_name, content)
dir = DEFAULT_ANNOTATIONS_DIR
FileUtils.mkdir_p(dir)
say("\n Fetched #{set_color(gem_name, :yellow, :bold)}", :green)
create_file("#{dir}/#{gem_name}.rbi", content)
end
- sig { params(path: String).returns(T.nilable(String)) }
- def fetch_file(path)
- if @central_repo_root_uri.start_with?(%r{https?://})
- fetch_http_file(path)
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
+ def fetch_file(repo_uri, path)
+ if repo_uri.start_with?(%r{https?://})
+ fetch_http_file(repo_uri, path)
else
- fetch_local_file(path)
+ fetch_local_file(repo_uri, path)
end
end
- sig { params(path: String).returns(T.nilable(String)) }
- def fetch_local_file(path)
- File.read("#{@central_repo_root_uri}/#{path}")
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
+ def fetch_local_file(repo_uri, path)
+ File.read("#{repo_uri}/#{path}")
rescue => e
say_error("\nCan't fetch file `#{path}` (#{e.message})", :bold, :red)
nil
end
- sig { params(path: String).returns(T.nilable(String)) }
- def fetch_http_file(path)
- uri = URI("#{@central_repo_root_uri}/#{path}")
- response = Net::HTTP.get_response(uri)
+ sig { params(repo_uri: String, path: String).returns(T.nilable(String)) }
+ def fetch_http_file(repo_uri, path)
+ auth = @tokens[repo_uri]
+ uri = URI("#{repo_uri}/#{path}")
+
+ request = Net::HTTP::Get.new(uri)
+ request["Authorization"] = auth if auth
+
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
+ http.request(request)
+ end
+
case response
when Net::HTTPSuccess
response.body
else
- say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{response.class})", :bold, :red)
+ say_http_error(path, repo_uri, message: response.class)
nil
end
rescue SocketError, Errno::ECONNREFUSED => e
- say_error("\nCan't fetch file `#{path}` from #{@central_repo_root_uri} (#{e.message})", :bold, :red)
+ say_http_error(path, repo_uri, message: e.message)
nil
end
sig { params(name: String, content: String).returns(String) }
def add_header(name, content)
header = <<~COMMENT
# DO NOT EDIT MANUALLY
- # This file was pulled from #{@central_repo_root_uri}.
+ # This file was pulled from a central RBI files repository.
# Please run `#{default_command(:annotations)}` to update it.
COMMENT
contents = content.split("\n")
if contents[0]&.start_with?("# typed:") && contents[1]&.empty?
contents.insert(2, header).join("\n")
else
say_error("Couldn't insert file header for content: #{content} due to unexpected file format")
content
end
+ end
+
+ sig { params(name: String, content: String).returns(String) }
+ def apply_typed_override(name, content)
+ strictness = @typed_overrides[name]
+ return content unless strictness
+
+ unless Spoom::Sorbet::Sigils.strictness_in_content(content)
+ return "# typed: #{strictness}\n\n#{content}"
+ end
+
+ Spoom::Sorbet::Sigils.update_sigil(content, strictness)
+ end
+
+ sig { params(gem_name: String, contents: T::Array[String]).returns(T.nilable(String)) }
+ def merge_files(gem_name, contents)
+ return nil if contents.empty?
+
+ rewriter = RBI::Rewriters::Merge.new(keep: RBI::Rewriters::Merge::Keep::NONE)
+
+ contents.each do |content|
+ rbi = RBI::Parser.parse_string(content)
+ rewriter.merge(rbi)
+ end
+
+ tree = rewriter.tree
+ return tree.string if tree.conflicts.empty?
+
+ say_error("\n\n Can't import RBI file for `#{gem_name}` as it contains conflicts:", :yellow)
+
+ tree.conflicts.each do |conflict|
+ say_error(" #{conflict}", :yellow)
+ end
+
+ nil
+ rescue RBI::ParseError => e
+ say_error("\n\n Can't import RBI file for `#{gem_name}` as it contains errors:", :yellow)
+ say_error(" Error: #{e.message} (#{e.location})")
+ nil
+ end
+
+ sig { returns(T::Hash[String, T.nilable(String)]) }
+ def repo_tokens
+ @netrc_info = Netrc.read(@netrc_file) if @netrc_file
+ @central_repo_root_uris.map do |uri|
+ if @auth
+ [uri, @auth]
+ else
+ [uri, token_for(uri)]
+ end
+ end.compact.to_h
+ end
+
+ sig { params(repo_uri: String).returns(T.nilable(String)) }
+ def token_for(repo_uri)
+ return nil unless @netrc_info
+
+ host = URI(repo_uri).host
+ return nil unless host
+
+ creds = @netrc_info[host]
+ return nil unless creds
+
+ token = creds.to_a.last
+ return nil unless token
+
+ "token #{token}"
+ end
+
+ sig { params(path: String, repo_uri: String, message: String).void }
+ def say_http_error(path, repo_uri, message:)
+ say_error("\nCan't fetch file `#{path}` from #{repo_uri} (#{message})\n\n", :bold, :red)
+ say_error(<<~ERROR)
+ Tapioca can't access the annotations at #{repo_uri}.
+
+ Are you trying to access a private repository?
+ If so, please specify your Github credentials in your ~/.netrc file or by specifying the --auth option.
+
+ See https://github.com/Shopify/tapioca#using-a-netrc-file for more details.
+ ERROR
end
end
end
end