Make git cl smarter about subcommands typos.
Look for either a unique prefix or for an approximation of the shortest
Levenshtein distance. So all of these will resolve to 'git cl upload':
git cl upl
git cl uplaod
These won't resolve:
git cl up # it shares prefix with 'upstream'
git cl uplao # not similar enough
Also align help against longest command instead of hard coded '10'. The help
page was distorded.
R=iannucci@chromium.org
BUG=
Review URL: https://chromiumcodereview.appspot.com/17272002
git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@206820 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/git_cl.py b/git_cl.py
index 7a43959..7e9180e 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -7,6 +7,7 @@
"""A git-command for integrating reviews on Rietveld."""
+import difflib
import json
import logging
import optparse
@@ -15,8 +16,8 @@
import stat
import sys
import textwrap
-import urlparse
import urllib2
+import urlparse
try:
import readline # pylint: disable=F0401,W0611
@@ -2019,8 +2020,43 @@
return 0
+### Glue code for subcommand handling.
+
+
+def Commands():
+ """Returns a dict of command and their handling function."""
+ module = sys.modules[__name__]
+ cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
+ return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
+
+
def Command(name):
- return getattr(sys.modules[__name__], 'CMD' + name, None)
+ """Retrieves the function to handle a command."""
+ commands = Commands()
+ if name in commands:
+ return commands[name]
+
+ # Try to be smart and look if there's something similar.
+ commands_with_prefix = [c for c in commands if c.startswith(name)]
+ if len(commands_with_prefix) == 1:
+ return commands[commands_with_prefix[0]]
+
+ # A #closeenough approximation of levenshtein distance.
+ def close_enough(a, b):
+ return difflib.SequenceMatcher(a=a, b=b).ratio()
+
+ hamming_commands = sorted(
+ ((close_enough(c, name), c) for c in commands),
+ reverse=True)
+ if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
+ # Too ambiguous.
+ return
+
+ if hamming_commands[0][0] < 0.8:
+ # Not similar enough. Don't be a fool and run a random command.
+ return
+
+ return commands[hamming_commands[0][1]]
def CMDhelp(parser, args):
@@ -2035,6 +2071,9 @@
def GenUsage(parser, command):
"""Modify an OptParse object with the function's documentation."""
obj = Command(command)
+ # Get back the real command name in case Command() guess the actual command
+ # name.
+ command = obj.__name__[3:]
more = getattr(obj, 'usage_more', '')
if command == 'help':
command = '<command>'
@@ -2058,9 +2097,13 @@
settings = Settings()
# Do it late so all commands are listed.
- CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
- ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
- for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
+ commands = Commands()
+ length = max(len(c) for c in commands)
+ docs = sorted(
+ (name, handler.__doc__.split('\n')[0].strip())
+ for name, handler in commands.iteritems())
+ CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
+ ' %-*s %s' % (length, name, doc) for name, doc in docs))
# Create the option parse and add --verbose support.
parser = optparse.OptionParser()