dispatch: provide help for disabled extensions and commands
authorBrodie Rao <me+hg@dackz.net>
Sun, 07 Feb 2010 14:01:43 +0100
changeset 10364 de1e7099d100
parent 10363 c07974215b3d
child 10365 d757bc0c7865
dispatch: provide help for disabled extensions and commands Before a command is declared unknown, each extension in hgext is searched, starting with hgext.<cmdname>. If there's a matching command, a help message suggests the appropriate extension and how to enable it. Every extension could potentially be imported, but for cases like rebase, relink, etc. only one extension is imported. For the case of "hg help disabledext", if the extension is in hgext, the extension description is read and a similar help suggestion is printed. No extension import occurs.
mercurial/commands.py
mercurial/dispatch.py
mercurial/extensions.py
mercurial/help.py
tests/test-extension
tests/test-extension.out
--- a/mercurial/commands.py	Sun Feb 07 11:32:08 2010 +0100
+++ b/mercurial/commands.py	Sun Feb 07 14:01:43 2010 +0100
@@ -1457,7 +1457,7 @@
         displayer.show(ctx)
     displayer.close()
 
-def help_(ui, name=None, with_version=False):
+def help_(ui, name=None, with_version=False, unknowncmd=False):
     """show help for a given topic or a help overview
 
     With no arguments, print a list of commands with short help messages.
@@ -1490,7 +1490,7 @@
             ui.write('\n')
 
         try:
-            aliases, entry = cmdutil.findcmd(name, table, False)
+            aliases, entry = cmdutil.findcmd(name, table, strict=unknowncmd)
         except error.AmbiguousCommand, inst:
             # py3k fix: except vars can't be used outside the scope of the
             # except block, nor can be used inside a lambda. python issue4617
@@ -1501,7 +1501,8 @@
 
         # check if it's an invalid alias and display its error if it is
         if getattr(entry[0], 'badalias', False):
-            entry[0](ui)
+            if not unknowncmd:
+                entry[0](ui)
             return
 
         # synopsis
@@ -1592,10 +1593,13 @@
     def helpext(name):
         try:
             mod = extensions.find(name)
+            doc = gettext(mod.__doc__) or _('no help text available')
         except KeyError:
-            raise error.UnknownCommand(name)
-
-        doc = gettext(mod.__doc__) or _('no help text available')
+            mod = None
+            doc = extensions.disabledext(name)
+            if not doc:
+                raise error.UnknownCommand(name)
+
         if '\n' not in doc:
             head, tail = doc, ""
         else:
@@ -1605,17 +1609,36 @@
             ui.write(minirst.format(tail, textwidth))
             ui.status('\n\n')
 
-        try:
-            ct = mod.cmdtable
-        except AttributeError:
-            ct = {}
-
-        modcmds = set([c.split('|', 1)[0] for c in ct])
-        helplist(_('list of commands:\n\n'), modcmds.__contains__)
+        if mod:
+            try:
+                ct = mod.cmdtable
+            except AttributeError:
+                ct = {}
+            modcmds = set([c.split('|', 1)[0] for c in ct])
+            helplist(_('list of commands:\n\n'), modcmds.__contains__)
+        else:
+            ui.write(_('use "hg help extensions" for information on enabling '
+                       'extensions\n'))
+
+    def helpextcmd(name):
+        cmd, ext, mod = extensions.disabledcmd(name, ui.config('ui', 'strict'))
+        doc = gettext(mod.__doc__).splitlines()[0]
+
+        msg = help.listexts(_("'%s' is provided by the following "
+                              "extension:") % cmd, {ext: doc}, len(ext),
+                            indent=4)
+        ui.write(minirst.format(msg, textwidth))
+        ui.write('\n\n')
+        ui.write(_('use "hg help extensions" for information on enabling '
+                   'extensions\n'))
 
     if name and name != 'shortlist':
         i = None
-        for f in (helptopic, helpcmd, helpext):
+        if unknowncmd:
+            queries = (helpextcmd,)
+        else:
+            queries = (helptopic, helpcmd, helpext, helpextcmd)
+        for f in queries:
             try:
                 f(name)
                 i = None
--- a/mercurial/dispatch.py	Sun Feb 07 11:32:08 2010 +0100
+++ b/mercurial/dispatch.py	Sun Feb 07 14:01:43 2010 +0100
@@ -93,7 +93,12 @@
         ui.warn(_("killed!\n"))
     except error.UnknownCommand, inst:
         ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
-        commands.help_(ui, 'shortlist')
+        try:
+            # check if the command is in a disabled extension
+            # (but don't check for extensions themselves)
+            commands.help_(ui, inst.args[0], unknowncmd=True)
+        except error.UnknownCommand:
+            commands.help_(ui, 'shortlist')
     except util.Abort, inst:
         ui.warn(_("abort: %s\n") % inst)
     except ImportError, inst:
@@ -218,6 +223,11 @@
             def fn(ui, *args):
                 ui.warn(_("alias '%s' resolves to unknown command '%s'\n") \
                             % (self.name, cmd))
+                try:
+                    # check if the command is in a disabled extension
+                    commands.help_(ui, cmd, unknowncmd=True)
+                except error.UnknownCommand:
+                    pass
                 return 1
             self.fn = fn
             self.badalias = True
--- a/mercurial/extensions.py	Sun Feb 07 11:32:08 2010 +0100
+++ b/mercurial/extensions.py	Sun Feb 07 14:01:43 2010 +0100
@@ -6,7 +6,7 @@
 # GNU General Public License version 2 or any later version.
 
 import imp, os
-import util, cmdutil, help
+import util, cmdutil, help, error
 from i18n import _, gettext
 
 _extensions = {}
@@ -131,8 +131,9 @@
     setattr(container, funcname, wrap)
     return origfn
 
-def _disabledpaths():
-    '''find paths of disabled extensions. returns a dict of {name: path}'''
+def _disabledpaths(strip_init=False):
+    '''find paths of disabled extensions. returns a dict of {name: path}
+    removes /__init__.py from packages if strip_init is True'''
     import hgext
     extpath = os.path.dirname(os.path.abspath(hgext.__file__))
     try: # might not be a filesystem path
@@ -150,6 +151,8 @@
             path = os.path.join(extpath, e, '__init__.py')
             if not os.path.exists(path):
                 continue
+            if strip_init:
+                path = os.path.dirname(path)
         if name in exts or name in _order or name == '__init__':
             continue
         exts[name] = path
@@ -191,6 +194,53 @@
 
     return exts, maxlength
 
+def disabledext(name):
+    '''find a specific disabled extension from hgext. returns desc'''
+    paths = _disabledpaths()
+    if name in paths:
+        return _disabledhelp(paths[name])
+
+def disabledcmd(cmd, strict=False):
+    '''import disabled extensions until cmd is found.
+    returns (cmdname, extname, doc)'''
+
+    paths = _disabledpaths(strip_init=True)
+    if not paths:
+        raise error.UnknownCommand(cmd)
+
+    def findcmd(cmd, name, path):
+        try:
+            mod = loadpath(path, 'hgext.%s' % name)
+        except Exception:
+            return
+        try:
+            aliases, entry = cmdutil.findcmd(cmd,
+                getattr(mod, 'cmdtable', {}), strict)
+        except (error.AmbiguousCommand, error.UnknownCommand):
+            return
+        for c in aliases:
+            if c.startswith(cmd):
+                cmd = c
+                break
+        else:
+            cmd = aliases[0]
+        return (cmd, name, mod)
+
+    # first, search for an extension with the same name as the command
+    path = paths.pop(cmd, None)
+    if path:
+        ext = findcmd(cmd, cmd, path)
+        if ext:
+            return ext
+
+    # otherwise, interrogate each extension until there's a match
+    for name, path in paths.iteritems():
+        ext = findcmd(cmd, name, path)
+        if ext:
+            return ext
+
+    raise error.UnknownCommand(cmd)
+
 def enabled():
     '''return a dict of {name: desc} of extensions, and the max name length'''
     exts = {}
--- a/mercurial/help.py	Sun Feb 07 11:32:08 2010 +0100
+++ b/mercurial/help.py	Sun Feb 07 14:01:43 2010 +0100
@@ -42,13 +42,14 @@
 
     return ''.join(result)
 
-def listexts(header, exts, maxlength):
+def listexts(header, exts, maxlength, indent=1):
     '''return a text listing of the given extensions'''
     if not exts:
         return ''
     result = '\n%s\n\n' % header
     for name, desc in sorted(exts.iteritems()):
-        result += ' %-*s %s\n' % (maxlength + 2, ':%s:' % name, desc)
+        result += '%s%-*s %s\n' % (' ' * indent, maxlength + 2,
+                                   ':%s:' % name, desc)
     return result
 
 def extshelp():
--- a/tests/test-extension	Sun Feb 07 11:32:08 2010 +0100
+++ b/tests/test-extension	Sun Feb 07 14:01:43 2010 +0100
@@ -153,3 +153,27 @@
 
 echo % show extensions
 hg debugextensions
+
+echo '% disabled extension commands'
+HGRCPATH=
+hg help email
+hg qdel
+hg churn
+echo '% disabled extensions'
+hg help churn
+hg help patchbomb
+echo '% broken disabled extension and command'
+mkdir hgext
+echo > hgext/__init__.py
+cat > hgext/broken.py <<EOF
+"broken extension'
+EOF
+TMPPYTHONPATH="$PYTHONPATH"
+PYTHONPATH="`pwd`:$PYTHONPATH"
+export PYTHONPATH
+hg help broken
+hg help foo > /dev/null
+PYTHONPATH="$TMPPYTHONPATH"
+export PYTHONPATH
+
+exit 0
--- a/tests/test-extension.out	Sun Feb 07 11:32:08 2010 +0100
+++ b/tests/test-extension.out	Sun Feb 07 14:01:43 2010 +0100
@@ -96,3 +96,33 @@
 % show extensions
 debugissue811
 mq
+% disabled extension commands
+'email' is provided by the following extension:
+
+    patchbomb  command to send changesets as (a series of) patch emails
+
+use "hg help extensions" for information on enabling extensions
+hg: unknown command 'qdel'
+'qdelete' is provided by the following extension:
+
+    mq  manage a stack of patches
+
+use "hg help extensions" for information on enabling extensions
+hg: unknown command 'churn'
+'churn' is provided by the following extension:
+
+    churn  command to display statistics about repository history
+
+use "hg help extensions" for information on enabling extensions
+% disabled extensions
+churn extension - command to display statistics about repository history
+
+use "hg help extensions" for information on enabling extensions
+patchbomb extension - command to send changesets as (a series of) patch emails
+
+use "hg help extensions" for information on enabling extensions
+% broken disabled extension and command
+broken extension - (no help text available)
+
+use "hg help extensions" for information on enabling extensions
+hg: unknown command 'foo'