diff options
Diffstat (limited to 'lib/rubygems/spec_fetcher.rb')
-rw-r--r-- | lib/rubygems/spec_fetcher.rb | 298 |
1 files changed, 110 insertions, 188 deletions
diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 7302ad9ffa..531d023b2f 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -2,6 +2,7 @@ require 'rubygems/remote_fetcher' require 'rubygems/user_interaction' require 'rubygems/errors' require 'rubygems/text' +require 'rubygems/name_tuple' ## # SpecFetcher handles metadata updates from remote gem repositories. @@ -11,17 +12,6 @@ class Gem::SpecFetcher include Gem::UserInteraction include Gem::Text - FILES = { - :all => 'specs', - :latest => 'latest_specs', - :prerelease => 'prerelease_specs', - } - - ## - # The SpecFetcher cache dir. - - attr_reader :dir # :nodoc: - ## # Cache of latest specs @@ -48,8 +38,6 @@ class Gem::SpecFetcher end def initialize - require 'fileutils' - @dir = File.join Gem.user_home, '.gem', 'specs' @update_cache = File.stat(Gem.user_home).uid == Process.uid @@ -60,144 +48,124 @@ class Gem::SpecFetcher @caches = { :latest => @latest_specs, :prerelease => @prerelease_specs, - :all => @specs + :released => @specs, } @fetcher = Gem::RemoteFetcher.fetcher end ## - # Returns the local directory to write +uri+ to. + # + # Find and fetch gem name tuples that match +dependency+. + # + # If +matching_platform+ is false, gems for all platforms are returned. - def cache_dir(uri) - # Correct for windows paths - escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/') - File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path) - end + def search_for_dependency(dependency, matching_platform=true) + found = {} - ## - # Fetch specs matching +dependency+. If +all+ is true, all matching - # (released) versions are returned. If +matching_platform+ is - # false, all platforms are returned. If +prerelease+ is true, - # prerelease versions are included. - - def fetch_with_errors(dependency, - all = false, - matching_platform = true, - prerelease = false) - - specs_and_sources, errors = find_matching_with_errors(dependency, - all, - matching_platform, - prerelease) - - ss = specs_and_sources.map do |spec_tuple, source_uri| - [fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri] + rejected_specs = {} + + if dependency.prerelease? + type = :complete + elsif dependency.latest_version? + type = :latest + else + type = :released end - return [ss, errors] - end + list, errors = available_specs(type) + list.each do |source, specs| + found[source] = specs.select do |tup| + if dependency.match?(tup) + if matching_platform and !Gem::Platform.match(tup.platform) + pm = ( + rejected_specs[dependency] ||= \ + Gem::PlatformMismatch.new(tup.name, tup.version)) + pm.add_platform tup.platform + false + else + true + end + end + end + end - def fetch(*args) - fetch_with_errors(*args).first - end + errors += rejected_specs.values - def fetch_spec(spec, source_uri) - source_uri = URI.parse source_uri if String === source_uri - spec = spec - [nil, 'ruby', ''] - spec_file_name = "#{spec.join '-'}.gemspec" + tuples = [] - uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}" + found.each do |source, specs| + specs.each do |s| + tuples << [s, source] + end + end - cache_dir = cache_dir uri + tuples = tuples.sort_by { |x| x[0] } - local_spec = File.join cache_dir, spec_file_name + return [tuples, errors] + end - if File.exist? local_spec then - spec = Gem.read_binary local_spec - else - uri.path << '.rz' - spec = @fetcher.fetch_path uri - spec = Gem.inflate spec + ## + # Return all gem name tuples who's names match +obj+ - if @update_cache then - FileUtils.mkdir_p cache_dir + def detect(type=:complete) + tuples = [] - open local_spec, 'wb' do |io| - io.write spec + list, _ = available_specs(type) + list.each do |source, specs| + specs.each do |tup| + if yield(tup) + tuples << [tup, source] end end end - # TODO: Investigate setting Gem::Specification#loaded_from to a URI - Marshal.load spec + tuples end + ## - # Find spec names that match +dependency+. If +all+ is true, all - # matching released versions are returned. If +matching_platform+ - # is false, gems for all platforms are returned. - - def find_matching_with_errors(dependency, - all = false, - matching_platform = true, - prerelease = false) - found = {} + # Find and fetch specs that match +dependency+. + # + # If +matching_platform+ is false, gems for all platforms are returned. - rejected_specs = {} + def spec_for_dependency(dependency, matching_platform=true) + tuples, errors = search_for_dependency(dependency, matching_platform) - list(all, prerelease).each do |source_uri, specs| - found[source_uri] = specs.select do |spec_name, version, spec_platform| - if dependency.match?(spec_name, version) - if matching_platform and !Gem::Platform.match(spec_platform) - pm = (rejected_specs[dependency] ||= Gem::PlatformMismatch.new(spec_name, version)) - pm.add_platform spec_platform - false - else - true - end - end + specs = [] + tuples.each do |tup, source| + begin + spec = source.fetch_spec(tup) + rescue Gem::RemoteFetcher::FetchError => e + errors << Gem::SourceFetchProblem.new(source, e) + else + specs << [spec, source] end end - errors = rejected_specs.values - - specs_and_sources = [] - - found.each do |source_uri, specs| - uri_str = source_uri.to_s - specs_and_sources.concat(specs.map { |spec| [spec, uri_str] }) - end - - [specs_and_sources, errors] - end - - def find_matching(*args) - find_matching_with_errors(*args).first + return [specs, errors] end ## - # Suggests a gem based on the supplied +gem_name+. Returns a string - # of the gem name if an approximate match can be found or nil - # otherwise. NOTE: for performance reasons only gems which exactly - # match the first character of +gem_name+ are considered. + # Suggests gems based on the supplied +gem_name+. Returns an array of + # alternative gem names. def suggest_gems_from_name gem_name - gem_name = gem_name.downcase + gem_name = gem_name.downcase.tr('_-', '') max = gem_name.size / 2 - specs = list.values.flatten 1 + names = available_specs(:complete).first.values.flatten(1) - matches = specs.map { |name, version, platform| - next unless Gem::Platform.match platform + matches = names.map { |n| + next unless n.match_platform? - distance = levenshtein_distance gem_name, name.downcase + distance = levenshtein_distance gem_name, n.name.downcase.tr('_-', '') next if distance >= max - return [name] if distance == 0 + return [n.name] if distance == 0 - [name, distance] + [n.name, distance] }.compact matches = matches.uniq.sort_by { |name, dist| dist } @@ -206,92 +174,46 @@ class Gem::SpecFetcher end ## - # Returns a list of gems available for each source in Gem::sources. If - # +all+ is true, all released versions are returned instead of only latest - # versions. If +prerelease+ is true, include prerelease versions. - - def list(all = false, prerelease = false) - # TODO: make type the only argument - type = if all - :all - elsif prerelease - :prerelease - else - :latest - end - - list = {} - file = FILES[type] - cache = @caches[type] - - Gem.sources.each do |source_uri| - source_uri = URI.parse source_uri - - unless cache.include? source_uri - cache[source_uri] = load_specs source_uri, file - end - - list[source_uri] = cache[source_uri] - end - - if type == :all - list.values.map do |gems| - gems.reject! { |g| !g[1] || g[1].prerelease? } - end - end - - list - end - - ## - # Loads specs in +file+, fetching from +source_uri+ if the on-disk cache is - # out of date. - - def load_specs(source_uri, file) - file_name = "#{file}.#{Gem.marshal_version}" - spec_path = source_uri + "#{file_name}.gz" - cache_dir = cache_dir spec_path - local_file = File.join(cache_dir, file_name) - loaded = false - - if File.exist? local_file then + # Returns a list of gems available for each source in Gem::sources. + # + # +type+ can be one of 3 values: + # :released => Return the list of all released specs + # :complete => Return the list of all specs + # :latest => Return the list of only the highest version of each gem + # :prerelease => Return the list of all prerelease only specs + # + + def available_specs(type) + errors = [] + list = {} + + Gem.sources.each_source do |source| begin - spec_dump = - @fetcher.fetch_path(spec_path, File.mtime(local_file)) + names = case type + when :latest + tuples_for source, :latest + when :released + tuples_for source, :released + when :complete + tuples_for(source, :prerelease) + tuples_for(source, :released) + when :prerelease + tuples_for(source, :prerelease) + else + raise Gem::Exception, "Unknown type - :#{type}" + end rescue Gem::RemoteFetcher::FetchError => e - alert_warning "Error fetching data: #{e.message}" + errors << Gem::SourceFetchProblem.new(source, e) + else + list[source] = names end - - loaded = true if spec_dump - - spec_dump ||= Gem.read_binary local_file - else - spec_dump = @fetcher.fetch_path spec_path - loaded = true end - specs = begin - Marshal.load spec_dump - rescue ArgumentError - spec_dump = @fetcher.fetch_path spec_path - loaded = true - - Marshal.load spec_dump - end - - if loaded and @update_cache then - begin - FileUtils.mkdir_p cache_dir - - open local_file, 'wb' do |io| - io << spec_dump - end - rescue - end - end - - specs + [list, errors] end + def tuples_for(source, type) + cache = @caches[type] + cache[source.uri] ||= source.load_specs(type) + end end |