From 967c0a8f797c1d41a49cbb6190f8015455037da7 Mon Sep 17 00:00:00 2001 From: "maruel@chromium.org" Date: Mon, 17 Jun 2013 22:52:24 +0000 Subject: [PATCH] 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 --- git_cl.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/git_cl.py b/git_cl.py index 7a43959726..7e9180e8a9 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 re import stat import sys import textwrap -import urlparse import urllib2 +import urlparse try: import readline # pylint: disable=F0401,W0611 @@ -2019,8 +2020,43 @@ def CMDformat(parser, args): 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 CMDhelp(parser, args): 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 = '' @@ -2058,9 +2097,13 @@ def main(argv): 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()