Skip to content

Commit

Permalink
+ Added fuzzy name matching on install failures. (gstark/presidentbeef)
Browse files Browse the repository at this point in the history
  • Loading branch information
zenspider committed Dec 29, 2010
1 parent 8f7f257 commit c9648b2
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 4 deletions.
12 changes: 10 additions & 2 deletions lib/rubygems/command.rb
Expand Up @@ -146,15 +146,23 @@ def execute
end

##
#
# 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, domain)
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"
end

unless domain == :local then # HACK
suggestions = Gem::SpecFetcher.fetcher.suggest_gems_from_name gem_name

unless suggestions.empty?
alert_error "Possible alternatives: #{suggestions.join(", ")}"
end
end
end

##
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/commands/fetch_command.rb
Expand Up @@ -52,7 +52,7 @@ def execute
spec, source_uri = specs_and_sources.sort_by { |s,| s.version }.last

if spec.nil? then
show_lookup_failure gem_name, version, errors
show_lookup_failure gem_name, version, errors, options[:domain]
next
end

Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/commands/install_command.rb
Expand Up @@ -128,7 +128,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, options[:domain]

exit_code |= 2
end
Expand Down
31 changes: 31 additions & 0 deletions lib/rubygems/spec_fetcher.rb
@@ -1,13 +1,15 @@
require 'rubygems/remote_fetcher'
require 'rubygems/user_interaction'
require 'rubygems/errors'
require 'rubygems/text'

##
# SpecFetcher handles metadata updates from remote gem repositories.

class Gem::SpecFetcher

include Gem::UserInteraction
include Gem::Text

##
# The SpecFetcher cache dir.
Expand Down Expand Up @@ -181,6 +183,35 @@ def legacy_repos
end
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.

def suggest_gems_from_name gem_name
gem_name = gem_name.downcase
gem_starts_with = gem_name[0,1]
max = gem_name.size / 2
specs = list.values.flatten(1) # flatten(1) is 1.8.7 and up

matches = specs.map { |name, version, platform|
next unless Gem::Platform.match platform

distance = levenshtein_distance gem_name, name.downcase

next if distance >= max

return [name] if distance == 0

[name, distance]
}.compact

matches = matches.uniq.sort_by { |name, dist| dist }

matches.first(5).map { |name, dist| name }
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
Expand Down
35 changes: 35 additions & 0 deletions lib/rubygems/text.rb
Expand Up @@ -26,5 +26,40 @@ def format_text(text, wrap, indent=0)
result.join("\n").gsub(/^/, " " * indent)
end

# This code is based directly on the Text gem implementation
# Returns a value representing the "cost" of transforming str1 into str2
def levenshtein_distance str1, str2
s = str1
t = str2
n = s.length
m = t.length
max = n/2

return m if (0 == n)
return n if (0 == m)
return n if (n - m).abs > max

d = (0..m).to_a
x = nil

n.times do |i|
e = i+1

m.times do |j|
cost = (s[i] == t[j]) ? 0 : 1
x = [
d[j+1] + 1, # insertion
e + 1, # deletion
d[j] + cost # substitution
].min
d[j] = e
e = x
end

d[m] = x
end

return x
end
end

23 changes: 23 additions & 0 deletions test/test_gem_commands_install_command.rb
Expand Up @@ -172,6 +172,29 @@ def test_execute_nonexistent
assert_match(/ould not find a valid gem 'nonexistent'/, @ui.error)
end

def test_execute_nonexistent_with_hint
misspelled="nonexistent_with_hint"
correctly_spelled="non_existent_with_hint"

util_setup_fake_fetcher
util_setup_spec_fetcher quick_gem(correctly_spelled, '2')

@cmd.options[:args] = misspelled

use_ui @ui do
e = assert_raises Gem::SystemExitException do
@cmd.execute
end
assert_equal 2, e.exit_code
end

expected = "ERROR: Could not find a valid gem 'nonexistent_with_hint' (>= 0) in any repository
ERROR: Possible alternatives: non_existent_with_hint
"

assert_equal expected, @ui.error
end

def test_execute_prerelease
util_setup_fake_fetcher(:prerelease)
util_setup_spec_fetcher @a2, @a2_pre
Expand Down
21 changes: 21 additions & 0 deletions test/test_gem_text.rb
Expand Up @@ -19,4 +19,25 @@ def test_format_text_none
def test_format_text_none_indent
assert_equal " text to wrap", format_text("text to wrap", 40, 2)
end

def test_levenshtein_distance_add
assert_equal 2, levenshtein_distance("zentest", "zntst")
assert_equal 2, levenshtein_distance("zntst", "zentest")
end

def test_levenshtein_distance_empty
assert_equal 5, levenshtein_distance("abcde", "")
assert_equal 5, levenshtein_distance("", "abcde")
end

def test_levenshtein_distance_remove
assert_equal 3, levenshtein_distance("zentest", "zentestxxx")
assert_equal 3, levenshtein_distance("zentestxxx", "zentest")
end

def test_levenshtein_distance_replace
assert_equal 2, levenshtein_distance("zentest", "ZenTest")
assert_equal 7, levenshtein_distance("xxxxxxx", "ZenTest")
assert_equal 7, levenshtein_distance("zentest", "xxxxxxx")
end
end

0 comments on commit c9648b2

Please sign in to comment.