Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/rubygems/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,15 @@ def execute
##
#
# Display to the user that a gem couldn't be found and reasons why
def show_lookup_failure(gem_name, version, errors=nil)
def show_lookup_failure(gem_name, version, errors=nil, suggestions=nil)
if errors and !errors.empty?
alert_error "Could not find a valid gem '#{gem_name}' (#{version}), here is why:"
errors.each { |x| say " #{x.wordy}" }
else
alert_error "Could not find a valid gem '#{gem_name}' (#{version}) in any repository"
if suggestions and not suggestions.empty?
say " Possible alternatives: #{suggestions.join(", ")}"
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/commands/install_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def execute
alert_error "Error installing #{gem_name}:\n\t#{e.message}"
exit_code |= 1
rescue Gem::GemNotFoundException => e
show_lookup_failure e.name, e.version, e.errors
show_lookup_failure e.name, e.version, e.errors, e.suggestions

exit_code |= 2
end
Expand Down
8 changes: 7 additions & 1 deletion lib/rubygems/dependency_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,15 @@ def find_spec_by_name_and_version(gem_name,
end

if spec_and_source.nil? then
if version != Gem::Requirement.default or @domain == :local
suggestions = nil
else
suggestions = Gem::SpecFetcher.fetcher.find_similar(gem_name)
end

raise Gem::GemNotFoundException.new(
"Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository",
gem_name, version, @errors)
gem_name, version, @errors, suggestions)
end

@specs_and_sources = [spec_and_source]
Expand Down
5 changes: 3 additions & 2 deletions lib/rubygems/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ class Gem::FormatException < Gem::Exception
end

class Gem::GemNotFoundException < Gem::Exception
def initialize(msg, name=nil, version=nil, errors=nil)
def initialize(msg, name=nil, version=nil, errors=nil, suggestions=nil)
super msg
@name = name
@version = version
@errors = errors
@suggestions = suggestions
end

attr_reader :name, :version, :errors
attr_reader :name, :version, :errors, :suggestions
end

class Gem::InstallError < Gem::Exception; end
Expand Down
98 changes: 98 additions & 0 deletions lib/rubygems/spec_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,56 @@ def find_matching(*args)
find_matching_with_errors(*args).first
end

##
# Returns an Array of gem names similar to +gem_name+.
#

def find_similar(gem_name, matching_platform = true)
matches = []
#Gem name argument is frozen
gem_name = gem_name.downcase
gem_name.gsub!(/_|-/, "")
limit = gem_name.length / 2 #Limit on how dissimilar two strings can be
match_limit = 12 #Limit on how many matches to return

early_break = false
list.each do |source_uri, specs|
specs.each do |spec_name, version, spec_platform|
next if matching_platform and !Gem::Platform.match(spec_platform)

normalized_spec_name = spec_name.downcase.gsub(/_|-/, "")

distance = similarity gem_name, normalized_spec_name, limit

#Consider this a perfect match and return it
if distance == 0
return [spec_name]
end

if distance < limit
matches << [spec_name, distance]
#Only stop early if it has at least inspected gem names starting with the same
#letter.
if matches.length > match_limit and normalized_spec_name[0,1] > gem_name[0,1]
early_break = true
break
end
end
end
break if early_break
end

#Sort by closest match
matches = matches.uniq.sort_by { |m| m[1] }.map { |m| m[0] }

#Limit number of results
if early_break
matches = matches[0, match_limit] << "..."
end

matches
end

##
# Returns Array of gem repositories that were generated with RubyGems less
# than 1.2.
Expand Down Expand Up @@ -274,6 +324,54 @@ def load_specs(source_uri, file)
end

##
# Returns a number indicating how similar the +input+ is to a given
# +gem_name+.
# +limit+ sets an upper limit on how dissimilar two strings can be, but the
# method may still return a higher number.
#
# 0 is considered a perfect match, but note the function does some processing
# on the strings, so this is not the same as +input+ == +gem_name+.

def similarity input, gem_name, limit
#Check for same string
return 0 if input == gem_name

len1 = input.length
len2 = gem_name.length

#Check if input matches beginning of gem name
#But not if the input is tiny
if len1 > 3 and gem_name[0, len1] == input
return limit - 1
end

#Check if string lengths are too different
return limit if (len1 - len2).abs > limit

#Compute Damerau–Levenshtein distance
#Based on http://gist.github.com/182759
oneago = nil
row = (1..len2).to_a << 0
len1.times do |i|
twoago, oneago, row = oneago, row, Array.new(len2) {0} << (i + 1)
len2.times do |j|
cost = input[i] == gem_name[j] ? 0 : 1
delcost = oneago[j] + 1
addcost = row[j - 1] + 1
subcost = oneago[j - 1] + cost
row[j] = [delcost, addcost, subcost].min
if i > 0 and j > 0 and input[i] == gem_name[j-1] and
input[i-1] == gem_name[j]
row[j] = [row[j], twoago[j - 2] + cost].min
end
end
end

row[len2 - 1]
end

private :similarity

# Warn about legacy repositories if +exception+ indicates only legacy
# repositories are available, and yield to the block. Returns false if the
# exception indicates some other FetchError.
Expand Down