Subject: [StGit PATCH 3/3] Generate command lists automatically

From: Karl Hasselström <[email-masked]>
Date: Mon, 08 Sep 2008 23:08:11 +0200

Instead of hard-coding the list of stg subcommands everywhere,
generate them automatically. That way, they never get outdated (like
the list in stg.txt).

In order to not make StGit slower, we cache the list of commands; run
"make build" to update that cache (other make targets, such as "all"
and "test", imply "build"). If the cache doesn't exist, the code falls
back to importing all the command modules, which adds quite a bit of
overhead to each stg command (about 100 ms on my laptop).

Signed-off-by: Karl Hasselström <[email-masked]>

---

 Documentation/Makefile      |    9 ++-
 Documentation/asciidoc.conf |   17 +++++
 Documentation/stg.txt       |  136 -------------------------------------------
 Makefile                    |   18 ++++--
 stg-build                   |   15 ++++-
 stgit/commands/.gitignore   |    1 
 stgit/commands/__init__.py  |   78 +++++++++++++++++++++++++
 stgit/commands/branch.py    |    1 
 stgit/commands/clean.py     |    1 
 stgit/commands/clone.py     |    1 
 stgit/commands/coalesce.py  |    1 
 stgit/commands/commit.py    |    1 
 stgit/commands/delete.py    |    1 
 stgit/commands/diff.py      |    1 
 stgit/commands/edit.py      |    1 
 stgit/commands/export.py    |    1 
 stgit/commands/files.py     |    1 
 stgit/commands/float.py     |    1 
 stgit/commands/fold.py      |    1 
 stgit/commands/goto.py      |    1 
 stgit/commands/hide.py      |    1 
 stgit/commands/id.py        |    1 
 stgit/commands/imprt.py     |    1 
 stgit/commands/init.py      |    1 
 stgit/commands/log.py       |    1 
 stgit/commands/mail.py      |    1 
 stgit/commands/new.py       |    1 
 stgit/commands/patches.py   |    1 
 stgit/commands/pick.py      |    1 
 stgit/commands/pop.py       |    1 
 stgit/commands/pull.py      |    1 
 stgit/commands/push.py      |    1 
 stgit/commands/rebase.py    |    1 
 stgit/commands/refresh.py   |    1 
 stgit/commands/rename.py    |    1 
 stgit/commands/repair.py    |    1 
 stgit/commands/resolved.py  |    1 
 stgit/commands/series.py    |    1 
 stgit/commands/show.py      |    1 
 stgit/commands/sink.py      |    1 
 stgit/commands/status.py    |    1 
 stgit/commands/sync.py      |    1 
 stgit/commands/top.py       |    1 
 stgit/commands/uncommit.py  |    1 
 stgit/commands/unhide.py    |    1 
 stgit/main.py               |  129 ++---------------------------------------
 46 files changed, 173 insertions(+), 268 deletions(-)
 create mode 100644 stgit/commands/.gitignore

diff --git a/Documentation/Makefile b/Documentation/Makefile
index 1c14fe3..85e9600 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -60,11 +60,16 @@ doc.dep : $(wildcard *.txt) build-docdep.perl
 -include doc.dep
 
 clean:
-	rm -f *.xml *.html *.pdf *.1 doc.dep $(COMMANDS_TXT)
+	rm -f *.xml *.html *.pdf *.1 doc.dep $(COMMANDS_TXT) stg-cmd-list.txt
 
-$(COMMANDS_TXT): $(shell find .. -name '*.py')
+ALL_PY = $(shell find ../stgit -name '*.py')
+
+$(COMMANDS_TXT): $(ALL_PY)
 	../stg-build --asciidoc $(basename $(subst stg-,,$@)) > $@
 
+stg-cmd-list.txt: $(ALL_PY)
+	../stg-build --cmd-list > $@
+
 %.html : %.txt
 	$(ASCIIDOC) -b xhtml11 -d manpage -f asciidoc.conf $(ASCIIDOC_EXTRA) $<
 
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
index 69447ab..5f7a7af 100644
--- a/Documentation/asciidoc.conf
+++ b/Documentation/asciidoc.conf
@@ -84,3 +84,20 @@ ifdef::backend-xhtml11[]
 [stglink-inlinemacro]
 <a href="stg-{target}.html">stg {target}</a>
 endif::backend-xhtml11[]
+
+## stgsublink: macro
+#
+# Usage: stgsublink:command[]
+#
+# Show StGit link as: <command> in man pages, stg <command> in
+# html.
+
+ifdef::backend-docbook[]
+[stgsublink-inlinemacro]
+{target}
+endif::backend-docbook[]
+
+ifdef::backend-xhtml11[]
+[stgsublink-inlinemacro]
+<a href="stg-{target}.html">{target}</a>
+endif::backend-xhtml11[]
diff --git a/Documentation/stg.txt b/Documentation/stg.txt
index a0b2176..b4184b2 100644
--- a/Documentation/stg.txt
+++ b/Documentation/stg.txt
@@ -105,141 +105,7 @@ description is available in individual command manpages.  Those
 manpages are named 'stg-<command>(1)'.
 endif::backend-docbook[]
 
-Generic commands
-~~~~~~~~~~~~~~~~
-
-User-support commands not touching the repository.
-
-stg help::
-	stgdesc:help[]
-stg version::
-	stgdesc:version[]
-stg copyright::
-	stgdesc:copyright[]
-
-Repository commands
-~~~~~~~~~~~~~~~~~~~
-
-stglink:clone[]::
-	stgdesc:clone[]
-stglink:id[]::
-	stgdesc:id[]
-
-Stack commands
-~~~~~~~~~~~~~~
-
-Stack management
-^^^^^^^^^^^^^^^^
-
-stglink:branch[]::
-	stgdesc:branch[]
-stglink:init[]::
-	stgdesc:init[]
-stglink:clean[]::
-	stgdesc:clean[]
-stglink:pull[]::
-	stgdesc:pull[]
-stglink:rebase[]::
-	stgdesc:rebase[]
-
-stglink:commit[]::
-	stgdesc:commit[]
-stglink:uncommit[]::
-	stgdesc:uncommit[]
-stglink:repair[]::
-	stgdesc:repair[]
-
-Controlling what patches are applied
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:series[]::
-	stgdesc:series[]
-stglink:push[]::
-	stgdesc:push[]
-stglink:pop[]::
-	stgdesc:pop[]
-stglink:goto[]::
-	stgdesc:goto[]
-stglink:float[]::
-	stgdesc:float[]
-stglink:sink[]::
-	stgdesc:sink[]
-stglink:applied[]::
-	stgdesc:applied[]
-stglink:unapplied[]::
-	stgdesc:unapplied[]
-stglink:top[]::
-	stgdesc:top[]
-
-stglink:hide[]::
-	stgdesc:hide[]
-stglink:unhide[]::
-	stgdesc:unhide[]
-
-Miscellaneous stack commands
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:patches[]::
-	stgdesc:patches[]
-
-
-Patch commands
-~~~~~~~~~~~~~~
-
-Patch management
-^^^^^^^^^^^^^^^^
-
-stglink:new[]::
-	stgdesc:new[]
-stglink:delete[]::
-	stgdesc:delete[]
-stglink:rename[]::
-	stgdesc:rename[]
-stglink:log[]::
-	stgdesc:log[]
-
-Controlling patch contents
-^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:files[]::
-	stgdesc:files[]
-stglink:show[]::
-	stgdesc:show[]
-stglink:refresh[]::
-	stgdesc:refresh[]
-stglink:fold[]::
-	stgdesc:fold[]
-stglink:pick[]::
-	stgdesc:pick[]
-stglink:sync[]::
-	stgdesc:sync[]
-
-Interaction with the rest of the world
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-stglink:export[]::
-	stgdesc:export[]
-stglink:import[]::
-	stgdesc:import[]
-stglink:mail[]::
-	stgdesc:mail[]
-
-
-Working-copy commands
-~~~~~~~~~~~~~~~~~~~~~
-
-stglink:add[]::
-	stgdesc:add[]
-stglink:rm[]::
-	stgdesc:rm[]
-stglink:cp[]::
-	stgdesc:cp[]
-stglink:status[]::
-	stgdesc:status[]
-stglink:diff[]::
-	stgdesc:diff[]
-stglink:resolved[]::
-	stgdesc:resolved[]
+include::stg-cmd-list.txt[]
 
 CONFIGURATION MECHANISM
 -----------------------
diff --git a/Makefile b/Makefile
index 9322fe0..288622a 100644
--- a/Makefile
+++ b/Makefile
@@ -4,10 +4,17 @@ PYTHON	?= python
 
 TEST_PATCHES ?= ..
 
-all:
+all: build
 	$(PYTHON) setup.py build
 
-install:
+build: stgit/commands/cmdlist.py
+
+ALL_PY = $(shell find stgit -name '*.py')
+
+stgit/commands/cmdlist.py: $(ALL_PY)
+	$(PYTHON) stg-build --py-cmd-list > $@
+
+install: build
 	$(PYTHON) setup.py install --prefix=$(prefix) --root=$(DESTDIR) --force
 
 doc:
@@ -19,10 +26,10 @@ install-doc:
 install-html:
 	$(MAKE) -C Documentation install-html
 
-test:
+test: build
 	cd t && $(MAKE) all
 
-test_patches:
+test_patches: build
 	for patch in $$(stg series --noprefix $(TEST_PATCHES)); do \
 		stg goto $$patch && $(MAKE) test || break; \
 	done
@@ -35,8 +42,9 @@ clean:
 	rm -f stgit/*.pyc
 	rm -f stgit/commands/*.pyc
 	rm -f TAGS
+	rm -f stgit/commands/cmdlist.py
 
 tags:
 	ctags -e -R stgit/*
 
-.PHONY: all install doc install-doc install-html test test_patches clean
+.PHONY: all build install doc install-doc install-html test test_patches clean
diff --git a/stg-build b/stg-build
index 8c39a6f..3c9dbfa 100755
--- a/stg-build
+++ b/stg-build
@@ -2,7 +2,7 @@
 # -*- python -*-
 import optparse, sys
 import stgit.main
-from stgit import argparse
+from stgit import argparse, commands
 
 def main():
     op = optparse.OptionParser()
@@ -10,6 +10,10 @@ def main():
                   help = 'Print asciidoc documentation for a command')
     op.add_option('--commands', action = 'store_true',
                   help = 'Print list of all stg subcommands')
+    op.add_option('--cmd-list', action = 'store_true',
+                  help = 'Print asciidoc command list')
+    op.add_option('--py-cmd-list', action = 'store_true',
+                  help = 'Write Python command list')
     options, args = op.parse_args()
     if args:
         op.error('Wrong number of arguments')
@@ -17,8 +21,15 @@ def main():
         argparse.write_asciidoc(stgit.main.commands[options.asciidoc],
                                 sys.stdout)
     elif options.commands:
-        for cmd in sorted(stgit.main.commands.iterkeys()):
+        for cmd in sorted(commands.get_commands(
+                allow_cached = False).iterkeys()):
             print cmd
+    elif options.cmd_list:
+        commands.asciidoc_command_list(
+            commands.get_commands(allow_cached = False), sys.stdout)
+    elif options.py_cmd_list:
+        commands.py_commands(commands.get_commands(allow_cached = False),
+                             sys.stdout)
     else:
         op.error('No command')
 
diff --git a/stgit/commands/.gitignore b/stgit/commands/.gitignore
new file mode 100644
index 0000000..eff10ee
--- /dev/null
+++ b/stgit/commands/.gitignore
@@ -0,0 +1 @@
+/cmdlist.py
diff --git a/stgit/commands/__init__.py b/stgit/commands/__init__.py
index 4b03e3a..b8e1ba5 100644
--- a/stgit/commands/__init__.py
+++ b/stgit/commands/__init__.py
@@ -1,5 +1,8 @@
+# -*- coding: utf-8 -*-
+
 __copyright__ = """
 Copyright (C) 2005, Catalin Marinas <[email-masked]>
+Copyright (C) 2008, Karl Hasselström <[email-masked]>
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License version 2 as
@@ -14,3 +17,78 @@ You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
+
+import os
+from stgit import utils
+
+def get_command(mod):
+    """Import and return the given command module."""
+    return __import__(__name__ + '.' + mod, fromlist = ['*'])
+
+_kinds = [('repo', 'Repository commands'),
+          ('stack', 'Stack (branch) commands'),
+          ('patch', 'Patch commands'),
+          ('wc', 'Index/worktree commands')]
+_kind_order = [kind for kind, desc in _kinds]
+_kinds = dict(_kinds)
+
+def _find_commands():
+    for p in __path__:
+        for fn in os.listdir(p):
+            if not fn.endswith('.py'):
+                continue
+            mod = utils.strip_suffix('.py', fn)
+            m = get_command(mod)
+            if not hasattr(m, 'usage'):
+                continue
+            yield mod, m
+
+def get_commands(allow_cached = True):
+    """Return a map from command name to a tuple of module name, command
+    type, and one-line command help."""
+    if allow_cached:
+        try:
+            from stgit.commands.cmdlist import command_list
+            return command_list
+        except ImportError:
+            # cmdlist.py doesn't exist, so do it the expensive way.
+            pass
+    return dict((getattr(m, 'name', mod), (mod, _kinds[m.kind], m.help))
+                for mod, m in _find_commands())
+
+def py_commands(commands, f):
+    f.write('command_list = {\n')
+    for key, val in sorted(commands.iteritems()):
+        f.write('    %r: %r,\n' % (key, val))
+    f.write('    }\n')
+
+def _command_list(commands):
+    kinds = {}
+    for cmd, (mod, kind, help) in commands.iteritems():
+        kinds.setdefault(kind, {})[cmd] = help
+    for kind in _kind_order:
+        kind = _kinds[kind]
+        yield kind, sorted(kinds[kind].iteritems())
+
+def pretty_command_list(commands, f):
+    cmd_len = max(len(cmd) for cmd in commands.iterkeys())
+    sep = ''
+    for kind, cmds in _command_list(commands):
+        f.write(sep)
+        sep = '\n'
+        f.write('%s:\n' % kind)
+        for cmd, help in cmds:
+            f.write('  %*s  %s\n' % (-cmd_len, cmd, help))
+
+def _write_underlined(s, u, f):
+    f.write(s + '\n')
+    f.write(u*len(s) + '\n')
+
+def asciidoc_command_list(commands, f):
+    for kind, cmds in _command_list(commands):
+        _write_underlined(kind, '~', f)
+        f.write('\n')
+        for cmd, help in cmds:
+            f.write('stgsublink:%s[]::\n' % cmd)
+            f.write('    %s\n' % help)
+        f.write('\n')
diff --git a/stgit/commands/branch.py b/stgit/commands/branch.py
index 33a8afe..1b1b98f 100644
--- a/stgit/commands/branch.py
+++ b/stgit/commands/branch.py
@@ -23,6 +23,7 @@ from stgit.out import *
 from stgit import stack, git, basedir
 
 help = 'Branch operations: switch, list, create, rename, delete, ...'
+kind = 'stack'
 usage = ['',
          '<branch>',
          '--list',
diff --git a/stgit/commands/clean.py b/stgit/commands/clean.py
index 5334704..27a7716 100644
--- a/stgit/commands/clean.py
+++ b/stgit/commands/clean.py
@@ -21,6 +21,7 @@ from stgit.commands import common
 from stgit.lib import transaction
 
 help = 'Delete the empty patches in the series'
+kind = 'stack'
 usage = ['']
 description = """
 Delete the empty patches in the whole series or only those applied or
diff --git a/stgit/commands/clone.py b/stgit/commands/clone.py
index b83169e..28500c5 100644
--- a/stgit/commands/clone.py
+++ b/stgit/commands/clone.py
@@ -21,6 +21,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Make a local clone of a remote repository'
+kind = 'repo'
 usage = ['<repository> <dir>']
 description = """
 Clone a git repository into the local directory <dir> (using
diff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py
index b390584..024668a 100644
--- a/stgit/commands/coalesce.py
+++ b/stgit/commands/coalesce.py
@@ -24,6 +24,7 @@ from stgit.commands import common
 from stgit.lib import git, transaction
 
 help = 'Coalesce two or more patches into one'
+kind = 'stack'
 usage = ['[options] <patches>']
 description = """
 Coalesce two or more patches, creating one big patch that contains all
diff --git a/stgit/commands/commit.py b/stgit/commands/commit.py
index 7d46724..99b7b5d 100644
--- a/stgit/commands/commit.py
+++ b/stgit/commands/commit.py
@@ -21,6 +21,7 @@ from stgit.lib import transaction
 from stgit.out import *
 
 help = 'Permanently store the applied patches into the stack base'
+kind = 'stack'
 usage = ['',
          '<patchnames>',
          '-n NUM',
diff --git a/stgit/commands/delete.py b/stgit/commands/delete.py
index 3ba7fc2..b92a039 100644
--- a/stgit/commands/delete.py
+++ b/stgit/commands/delete.py
@@ -21,6 +21,7 @@ from stgit.commands import common
 from stgit.lib import transaction
 
 help = 'Delete patches'
+kind = 'patch'
 usage = ['[options] <patch1> [<patch2>] [<patch3>..<patch4>]']
 description = """
 Delete the patches passed as arguments.
diff --git a/stgit/commands/diff.py b/stgit/commands/diff.py
index 47e2e7f..e0078f9 100644
--- a/stgit/commands/diff.py
+++ b/stgit/commands/diff.py
@@ -25,6 +25,7 @@ from stgit.out import *
 from stgit import argparse, stack, git
 
 help = 'Show the tree diff'
+kind = 'wc'
 usage = ['[options] [<files or dirs>]']
 description = """
 Show the diff (default) or diffstat between the current working copy
diff --git a/stgit/commands/edit.py b/stgit/commands/edit.py
index 0db3254..b6ef6c7 100644
--- a/stgit/commands/edit.py
+++ b/stgit/commands/edit.py
@@ -25,6 +25,7 @@ from stgit.lib import git as gitlib, transaction
 from stgit.out import *
 
 help = 'edit a patch description or diff'
+kind = 'patch'
 usage = ['[options] [<patch>]']
 description = """
 Edit the description and author information of the given patch (or the
diff --git a/stgit/commands/export.py b/stgit/commands/export.py
index 3d74cc8..8d05996 100644
--- a/stgit/commands/export.py
+++ b/stgit/commands/export.py
@@ -26,6 +26,7 @@ from stgit.out import out
 from stgit.lib import git as gitlib
 
 help = 'Export patches to a directory'
+kind = 'patch'
 usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = """
 Export a range of applied patches to a given directory (defaults to
diff --git a/stgit/commands/files.py b/stgit/commands/files.py
index 08f882c..318a4a3 100644
--- a/stgit/commands/files.py
+++ b/stgit/commands/files.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit import argparse, stack, git
 
 help = 'Show the files modified by a patch (or the current patch)'
+kind = 'patch'
 usage = ['[options] [[<branch>:]<patch>]']
 description = """
 List the files modified by the given patch (defaulting to the current
diff --git a/stgit/commands/float.py b/stgit/commands/float.py
index ac8eaea..1ca4ed3 100644
--- a/stgit/commands/float.py
+++ b/stgit/commands/float.py
@@ -23,6 +23,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Push patches to the top, even if applied'
+kind = 'stack'
 usage = ['<patches>',
          '-s <series>']
 description = """
diff --git a/stgit/commands/fold.py b/stgit/commands/fold.py
index 6ddaea9..0f1486a 100644
--- a/stgit/commands/fold.py
+++ b/stgit/commands/fold.py
@@ -23,6 +23,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Integrate a GNU diff patch into the current patch'
+kind = 'patch'
 usage = ['[options] [<file>]']
 description = """
 Apply the given GNU diff file (or the standard input) onto the top of
diff --git a/stgit/commands/goto.py b/stgit/commands/goto.py
index 33b20ed..2bfef0f 100644
--- a/stgit/commands/goto.py
+++ b/stgit/commands/goto.py
@@ -19,6 +19,7 @@ from stgit.commands import common
 from stgit.lib import transaction
 
 help = 'Push or pop patches to the given one'
+kind = 'stack'
 usage = ['<patch-name>']
 description = """
 Push/pop patches to/from the stack until the one given on the command
diff --git a/stgit/commands/hide.py b/stgit/commands/hide.py
index 488ea18..bee2162 100644
--- a/stgit/commands/hide.py
+++ b/stgit/commands/hide.py
@@ -23,6 +23,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Hide a patch in the series'
+kind = 'stack'
 usage = ['[options] <patch-range>']
 description = """
 Hide a range of unapplied patches so that they are no longer shown in
diff --git a/stgit/commands/id.py b/stgit/commands/id.py
index 18bab97..857ec33 100644
--- a/stgit/commands/id.py
+++ b/stgit/commands/id.py
@@ -20,6 +20,7 @@ from stgit.commands import common
 from stgit.lib import stack
 
 help = 'Print the git hash value of a StGit reference'
+kind = 'repo'
 usage = ['[options] [id]']
 description = """
 Print the SHA1 value of a Git id (defaulting to HEAD). In addition to
diff --git a/stgit/commands/imprt.py b/stgit/commands/imprt.py
index 1f813a2..e81475d 100644
--- a/stgit/commands/imprt.py
+++ b/stgit/commands/imprt.py
@@ -26,6 +26,7 @@ from stgit import argparse, stack, git
 
 name = 'import'
 help = 'Import a GNU diff file as a new patch'
+kind = 'patch'
 usage = ['[options] [<file>|<url>]']
 description = """
 Create a new patch and apply the given GNU diff file (or the standard
diff --git a/stgit/commands/init.py b/stgit/commands/init.py
index f7f7a21..67d20d1 100644
--- a/stgit/commands/init.py
+++ b/stgit/commands/init.py
@@ -20,6 +20,7 @@ from stgit.commands import common
 from stgit.lib import stack
 
 help = 'Initialise the current branch for use with StGIT'
+kind = 'stack'
 usage = ['']
 description = """
 Initialise the current git branch to be used as an StGIT stack. The
diff --git a/stgit/commands/log.py b/stgit/commands/log.py
index b505ed5..de210ea 100644
--- a/stgit/commands/log.py
+++ b/stgit/commands/log.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit.run import Run
 
 help = 'Display the patch changelog'
+kind = 'patch'
 usage = ['[options] [patch]']
 description = """
 List all the current and past commit ids of the given patch. The
diff --git a/stgit/commands/mail.py b/stgit/commands/mail.py
index 83acc86..caf8f9e 100644
--- a/stgit/commands/mail.py
+++ b/stgit/commands/mail.py
@@ -26,6 +26,7 @@ from stgit.config import config
 from stgit.run import Run
 
 help = 'Send a patch or series of patches by e-mail'
+kind = 'patch'
 usage = [' [options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = r"""
 Send a patch or a range of patches by e-mail using the SMTP server
diff --git a/stgit/commands/new.py b/stgit/commands/new.py
index 5e9d4ec..c1457bd 100644
--- a/stgit/commands/new.py
+++ b/stgit/commands/new.py
@@ -21,6 +21,7 @@ from stgit.commands import common
 from stgit.lib import git as gitlib, transaction
 
 help = 'Create a new, empty patch'
+kind = 'patch'
 usage = ['[options] [<name>]']
 description = """
 Create a new, empty patch on the current stack. The new patch is
diff --git a/stgit/commands/patches.py b/stgit/commands/patches.py
index 656a4a1..0cbc275 100644
--- a/stgit/commands/patches.py
+++ b/stgit/commands/patches.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Show the applied patches modifying a file'
+kind = 'stack'
 usage = ['[options] [<files or dirs>]']
 description = """
 Show the applied patches modifying the given files. Without arguments,
diff --git a/stgit/commands/pick.py b/stgit/commands/pick.py
index 72b2359..8a88262 100644
--- a/stgit/commands/pick.py
+++ b/stgit/commands/pick.py
@@ -24,6 +24,7 @@ from stgit import stack, git
 from stgit.stack import Series
 
 help = 'Import a patch from a different branch or a commit object'
+kind = 'patch'
 usage = ['[options] ([<patch1>] [<patch2>] [<patch3>..<patch4>])|<commit>']
 description = """
 Import one or more patches from a different branch or a commit object
diff --git a/stgit/commands/pop.py b/stgit/commands/pop.py
index b3c4008..cf89846 100644
--- a/stgit/commands/pop.py
+++ b/stgit/commands/pop.py
@@ -23,6 +23,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Pop one or more patches from the stack'
+kind = 'stack'
 usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = """
 Pop the topmost patch or a range of patches from the stack. The
diff --git a/stgit/commands/pull.py b/stgit/commands/pull.py
index 3801bf2..c989b5d 100644
--- a/stgit/commands/pull.py
+++ b/stgit/commands/pull.py
@@ -24,6 +24,7 @@ from stgit.config import GitConfigException
 from stgit import stack, git
 
 help = 'Pull changes from a remote repository'
+kind = 'stack'
 usage = ['[options] [<repository>]']
 description = """
 Pull the latest changes from the given remote repository (defaulting
diff --git a/stgit/commands/push.py b/stgit/commands/push.py
index ee7b56e..c6056cc 100644
--- a/stgit/commands/push.py
+++ b/stgit/commands/push.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Push one or more patches onto the stack'
+kind = 'stack'
 usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = """
 Push one or more patches (defaulting to the first unapplied one) onto
diff --git a/stgit/commands/rebase.py b/stgit/commands/rebase.py
index ffec329..b09204e 100644
--- a/stgit/commands/rebase.py
+++ b/stgit/commands/rebase.py
@@ -22,6 +22,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Move the stack base to another point in history'
+kind = 'stack'
 usage = ['[options] <new-base-id>']
 description = """
 Pop all patches from current stack, move the stack base to the given
diff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py
index e30085b..a20fcc5 100644
--- a/stgit/commands/refresh.py
+++ b/stgit/commands/refresh.py
@@ -25,6 +25,7 @@ from stgit import stack, git
 from stgit.config import config
 
 help = 'Generate a new commit for the current patch'
+kind = 'patch'
 usage = ['[options] [<files or dirs>]']
 description = """
 Include the latest tree changes in the current patch. This command
diff --git a/stgit/commands/rename.py b/stgit/commands/rename.py
index b978ecf..fdb31ee 100644
--- a/stgit/commands/rename.py
+++ b/stgit/commands/rename.py
@@ -23,6 +23,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Rename a patch'
+kind = 'patch'
 usage = ['[options] [oldpatch] <newpatch>']
 description = """
 Rename <oldpatch> into <newpatch> in a series. If <oldpatch> is not
diff --git a/stgit/commands/repair.py b/stgit/commands/repair.py
index a5156cd..6218caa 100644
--- a/stgit/commands/repair.py
+++ b/stgit/commands/repair.py
@@ -26,6 +26,7 @@ from stgit.run import *
 from stgit import stack, git
 
 help = 'Fix StGit metadata if branch was modified with git commands'
+kind = 'stack'
 usage = ['']
 description = """
 If you modify an StGit stack (branch) with some git commands -- such
diff --git a/stgit/commands/resolved.py b/stgit/commands/resolved.py
index 5643da8..d8dacd6 100644
--- a/stgit/commands/resolved.py
+++ b/stgit/commands/resolved.py
@@ -25,6 +25,7 @@ from stgit.config import config, file_extensions
 from stgit.gitmergeonefile import interactive_merge
 
 help = 'Mark a file conflict as solved'
+kind = 'wc'
 usage = ['[options] [<files...>]']
 description = """
 Mark a merge conflict as resolved. The conflicts can be seen with the
diff --git a/stgit/commands/series.py b/stgit/commands/series.py
index 6579a8a..492a3aa 100644
--- a/stgit/commands/series.py
+++ b/stgit/commands/series.py
@@ -22,6 +22,7 @@ from stgit.commands.common import parse_patches
 from stgit.out import out
 
 help = 'Print the patch series'
+kind = 'stack'
 usage = ['[options] [<patch-range>]']
 description = """
 Show all the patches in the series or just those in the given
diff --git a/stgit/commands/show.py b/stgit/commands/show.py
index c12793f..41cb31e 100644
--- a/stgit/commands/show.py
+++ b/stgit/commands/show.py
@@ -22,6 +22,7 @@ from stgit.commands.common import *
 from stgit import argparse, git
 
 help = 'Show the commit corresponding to a patch'
+kind = 'patch'
 usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = """
 Show the commit log and the diff corresponding to the given patches.
diff --git a/stgit/commands/sink.py b/stgit/commands/sink.py
index 11fae3a..2090cd4 100644
--- a/stgit/commands/sink.py
+++ b/stgit/commands/sink.py
@@ -23,6 +23,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Send patches deeper down the stack'
+kind = 'stack'
 usage = ['[-t <target patch>] [-n] [<patches>]']
 description = """
 This is the opposite operation of stglink:float[]: move the specified
diff --git a/stgit/commands/status.py b/stgit/commands/status.py
index eadb507..7c68ba6 100644
--- a/stgit/commands/status.py
+++ b/stgit/commands/status.py
@@ -23,6 +23,7 @@ from stgit.utils import *
 from stgit import stack, git
 
 help = 'Show the tree status'
+kind = 'wc'
 usage = ['[options] [<files or dirs>]']
 description = """
 Show the status of the whole working copy or the given files. The
diff --git a/stgit/commands/sync.py b/stgit/commands/sync.py
index ef94439..767b4d2 100644
--- a/stgit/commands/sync.py
+++ b/stgit/commands/sync.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Synchronise patches with a branch or a series'
+kind = 'patch'
 usage = ['[options] [<patch1>] [<patch2>] [<patch3>..<patch4>]']
 description = """
 For each of the specified patches perform a three-way merge with the
diff --git a/stgit/commands/top.py b/stgit/commands/top.py
index f4962e7..523afa4 100644
--- a/stgit/commands/top.py
+++ b/stgit/commands/top.py
@@ -21,6 +21,7 @@ from stgit.commands import common
 from stgit.out import out
 
 help = 'Print the name of the top patch'
+kind = 'stack'
 usage = ['']
 description = """
 Print the name of the current (topmost) patch."""
diff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py
index 19ec3c9..c44385d 100644
--- a/stgit/commands/uncommit.py
+++ b/stgit/commands/uncommit.py
@@ -24,6 +24,7 @@ from stgit.out import *
 from stgit import utils
 
 help = 'Turn regular git commits into StGit patches'
+kind = 'stack'
 usage = ['<patch-name-1> [<patch-name-2> ...]',
          '-n NUM [<prefix>]',
          '-t <committish> [-x]']
diff --git a/stgit/commands/unhide.py b/stgit/commands/unhide.py
index 6c624d0..c34bc1d 100644
--- a/stgit/commands/unhide.py
+++ b/stgit/commands/unhide.py
@@ -23,6 +23,7 @@ from stgit.out import *
 from stgit import stack, git
 
 help = 'Unhide a hidden patch'
+kind = 'stack'
 usage = ['[options] <patch-range>']
 description = """
 Unhide a hidden range of patches so that they are shown in the plain
diff --git a/stgit/main.py b/stgit/main.py
index 55c467c..48d8dbb 100644
--- a/stgit/main.py
+++ b/stgit/main.py
@@ -48,107 +48,13 @@ class Commands(dict):
         return candidates[0]
         
     def __getitem__(self, key):
-        """Return the command python module name based.
-        """
-        global prog
-
         cmd_mod = self.get(key) or self.get(self.canonical_cmd(key))
-            
-        __import__('stgit.commands.' + cmd_mod)
-        return getattr(stgit.commands, cmd_mod)
-
-commands = Commands({
-    'branch':           'branch',
-    'delete':           'delete',
-    'diff':             'diff',
-    'clean':            'clean',
-    'clone':            'clone',
-    'coalesce':         'coalesce',
-    'commit':           'commit',
-    'edit':             'edit',
-    'export':           'export',
-    'files':            'files',
-    'float':            'float',
-    'fold':             'fold',
-    'goto':             'goto',
-    'hide':             'hide',
-    'id':               'id',
-    'import':           'imprt',
-    'init':             'init',
-    'log':              'log',
-    'mail':             'mail',
-    'new':              'new',
-    'patches':          'patches',
-    'pick':             'pick',
-    'pop':              'pop',
-    'pull':             'pull',
-    'push':             'push',
-    'rebase':           'rebase',
-    'refresh':          'refresh',
-    'rename':           'rename',
-    'repair':           'repair',
-    'resolved':         'resolved',
-    'series':           'series',
-    'show':             'show',
-    'sink':             'sink',
-    'status':           'status',
-    'sync':             'sync',
-    'top':              'top',
-    'uncommit':         'uncommit',
-    'unhide':           'unhide'
-    })
+        return stgit.commands.get_command(cmd_mod)
 
-# classification: repository, stack, patch, working copy
-repocommands = (
-    'clone',
-    'id',
-    )
-stackcommands = (
-    'branch',
-    'clean',
-    'coalesce',
-    'commit',
-    'float',
-    'goto',
-    'hide',
-    'init',
-    'patches',
-    'pop',
-    'pull',
-    'push',
-    'rebase',
-    'repair',
-    'series',
-    'sink',
-    'top',
-    'uncommit',
-    'unhide',
-    )
-patchcommands = (
-    'delete',
-    'edit',
-    'export',
-    'files',
-    'fold',
-    'import',
-    'log',
-    'mail',
-    'new',
-    'pick',
-    'refresh',
-    'rename',
-    'show',
-    'sync',
-    )
-wccommands = (
-    'diff',
-    'resolved',
-    'status',
-    )
+cmd_list = stgit.commands.get_commands()
+commands = Commands((cmd, mod) for cmd, (mod, kind, help)
+                    in cmd_list.iteritems())
 
-def _print_helpstring(cmd):
-    print '  ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help
-    
 def print_help():
     print 'usage: %s <command> [options]' % os.path.basename(sys.argv[0])
     print
@@ -156,33 +62,8 @@ def print_help():
     print '  help        print the detailed command usage'
     print '  version     display version information'
     print '  copyright   display copyright information'
-    # unclassified commands if any
-    cmds = commands.keys()
-    cmds.sort()
-    for cmd in cmds:
-        if not cmd in repocommands and not cmd in stackcommands \
-               and not cmd in patchcommands and not cmd in wccommands:
-            _print_helpstring(cmd)
-    print
-
-    print 'Repository commands:'
-    for cmd in repocommands:
-        _print_helpstring(cmd)
     print
-    
-    print 'Stack commands:'
-    for cmd in stackcommands:
-        _print_helpstring(cmd)
-    print
-
-    print 'Patch commands:'
-    for cmd in patchcommands:
-        _print_helpstring(cmd)
-    print
-
-    print 'Working-copy commands:'
-    for cmd in wccommands:
-        _print_helpstring(cmd)
+    stgit.commands.pretty_command_list(cmd_list, sys.stdout)
 
 #
 # The main function (command dispatcher)