Automatic bash completion
authorKarl Hasselström <kha@treskal.com>
Sun, 21 Sep 2008 12:20:41 +0000 (14:20 +0200)
committerKarl Hasselström <kha@treskal.com>
Sun, 21 Sep 2008 12:20:41 +0000 (14:20 +0200)
Teach the build process to create a bash tab completion script. This
has three benefits:

  1. The tab completion gets faster, since it no longer has to run stg
     to figure out the set of available flags for each command.

  2. The tab completion script used to encode the list of stg
     subcommands, and the kind of arguments each subcommand expected.
     This information now lives in just one place: the subcommand's
     module.

  3. The tab completion script now knows what kind of argument each
     flag wants, and can tab complete those as well. So "stg refresh
     <tab>" will complete dirty files, but "stg refresh -p <tab>" will
     complete patch names.

Signed-off-by: Karl Hasselström <kha@treskal.com>
48 files changed:
.gitignore
Makefile
contrib/stgit-completion.bash [deleted file]
setup.py
stg-build
stgit/argparse.py
stgit/commands/branch.py
stgit/commands/clean.py
stgit/commands/clone.py
stgit/commands/coalesce.py
stgit/commands/commit.py
stgit/commands/delete.py
stgit/commands/diff.py
stgit/commands/edit.py
stgit/commands/export.py
stgit/commands/files.py
stgit/commands/float.py
stgit/commands/fold.py
stgit/commands/goto.py
stgit/commands/hide.py
stgit/commands/id.py
stgit/commands/imprt.py
stgit/commands/init.py
stgit/commands/log.py
stgit/commands/mail.py
stgit/commands/new.py
stgit/commands/patches.py
stgit/commands/pick.py
stgit/commands/pop.py
stgit/commands/pull.py
stgit/commands/push.py
stgit/commands/rebase.py
stgit/commands/redo.py
stgit/commands/refresh.py
stgit/commands/rename.py
stgit/commands/repair.py
stgit/commands/reset.py
stgit/commands/resolved.py
stgit/commands/series.py
stgit/commands/show.py
stgit/commands/sink.py
stgit/commands/status.py
stgit/commands/sync.py
stgit/commands/top.py
stgit/commands/uncommit.py
stgit/commands/undo.py
stgit/commands/unhide.py
stgit/completion.py [new file with mode: 0644]

index 91dbad2..e7fffb0 100644 (file)
@@ -6,3 +6,4 @@ patches-*
 release.sh
 setup.cfg.rpm
 snapshot.sh
+stgit-completion.bash
index 288622a..7183670 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -7,13 +7,16 @@ TEST_PATCHES ?= ..
 all: build
        $(PYTHON) setup.py build
 
-build: stgit/commands/cmdlist.py
+build: stgit/commands/cmdlist.py stgit-completion.bash
 
 ALL_PY = $(shell find stgit -name '*.py')
 
 stgit/commands/cmdlist.py: $(ALL_PY)
        $(PYTHON) stg-build --py-cmd-list > $@
 
+stgit-completion.bash: $(ALL_PY)
+       $(PYTHON) stg-build --bash-completion > $@
+
 install: build
        $(PYTHON) setup.py install --prefix=$(prefix) --root=$(DESTDIR) --force
 
diff --git a/contrib/stgit-completion.bash b/contrib/stgit-completion.bash
deleted file mode 100644 (file)
index 1467c28..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-# bash completion support for StGIT                        -*- shell-script -*-
-#
-# Copyright (C) 2006, Karl Hasselström <kha@treskal.com>
-# Based on git-completion.sh
-#
-# To use these routines:
-#
-#    1. Copy this file to somewhere (e.g. ~/.stgit-completion.bash).
-#
-#    2. Add the following line to your .bashrc:
-#         . ~/.stgit-completion.bash
-
-_stg_commands="
-    branch
-    delete
-    diff
-    clean
-    clone
-    coalesce
-    commit
-    edit
-    export
-    files
-    float
-    fold
-    goto
-    hide
-    id
-    import
-    init
-    log
-    mail
-    new
-    patches
-    pick
-    pop
-    pull
-    push
-    rebase
-    refresh
-    rename
-    repair
-    resolved
-    series
-    show
-    sink
-    status
-    sync
-    top
-    uncommit
-    unhide
-"
-
-# The path to .git, or empty if we're not in a repository.
-_gitdir ()
-{
-    echo "$(git rev-parse --git-dir 2>/dev/null)"
-}
-
-# Name of the current branch, or empty if there isn't one.
-_current_branch ()
-{
-    local b=$(git symbolic-ref HEAD 2>/dev/null)
-    echo ${b#refs/heads/}
-}
-
-# List of all applied patches.
-_applied_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/applied"
-}
-
-# List of all unapplied patches.
-_unapplied_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/unapplied"
-}
-
-# List of all applied patches.
-_hidden_patches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$(_current_branch)/hidden"
-}
-
-# List of all patches.
-_all_patches ()
-{
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied"
-}
-
-# List of all patches except the current patch.
-_all_other_patches ()
-{
-    local b=$(_current_branch)
-    local g=$(_gitdir)
-    [ "$g" ] && cat "$g/patches/$b/applied" "$g/patches/$b/unapplied" \
-        | grep -v "^$(cat $g/patches/$b/current 2> /dev/null)$"
-}
-
-_all_branches ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && (cd $g/patches/ && echo *)
-}
-
-_conflicting_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --conflict
-}
-
-_dirty_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --modified --new --deleted
-}
-
-_unknown_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && stg status --unknown
-}
-
-_known_files ()
-{
-    local g=$(_gitdir)
-    [ "$g" ] && git ls-files
-}
-
-# List the command options
-_cmd_options ()
-{
-    stg $1 --help 2>/dev/null | grep -e " --[A-Za-z]" | sed -e "s/.*\(--[^ =]\+\).*/\1/"
-}
-
-# Generate completions for patches and patch ranges from the given
-# patch list function, and options from the given list.
-_complete_patch_range ()
-{
-    local patchlist="$1" options="$2"
-    local pfx cur="${COMP_WORDS[COMP_CWORD]}"
-    case "$cur" in
-        *..*)
-            pfx="${cur%..*}.."
-            cur="${cur#*..}"
-            COMPREPLY=($(compgen -P "$pfx" -W "$($patchlist)" -- "$cur"))
-            ;;
-        *)
-            COMPREPLY=($(compgen -W "$options $($patchlist)" -- "$cur"))
-            ;;
-    esac
-}
-
-_complete_patch_range_options ()
-{
-    local patchlist="$1" options="$2" patch_options="$3"
-    local prev="${COMP_WORDS[COMP_CWORD-1]}"
-    local cur="${COMP_WORDS[COMP_CWORD]}"
-    local popt
-    for popt in $patch_options; do
-        if [ $prev == $popt ]; then
-            _complete_patch_range $patchlist
-            return
-        fi
-    done
-    COMPREPLY=($(compgen -W "$options" -- "$cur"))
-}
-
-_complete_branch ()
-{
-     COMPREPLY=($(compgen -W "$(_cmd_options $1) $($2)" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-# Generate completions for options from the given list.
-_complete_options ()
-{
-    local options="$1"
-    COMPREPLY=($(compgen -W "$options" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-_complete_files ()
-{
-    COMPREPLY=($(compgen -W "$(_cmd_options $1) $2" -- "${COMP_WORDS[COMP_CWORD]}"))
-}
-
-_stg_common ()
-{
-    _complete_options "$(_cmd_options $1)"
-}
-
-_stg_patches ()
-{
-    _complete_patch_range "$2" "$(_cmd_options $1)"
-}
-
-_stg_patches_options ()
-{
-    _complete_patch_range_options "$2" "$(_cmd_options $1)" "$3"
-}
-
-_stg_help ()
-{
-    _complete_options "$_stg_commands"
-}
-
-_stg ()
-{
-    local i c=1 command
-
-    while [ $c -lt $COMP_CWORD ]; do
-        if [ $c == 1 ]; then
-            command="${COMP_WORDS[c]}"
-        fi
-        c=$((++c))
-    done
-
-    # Complete name of subcommand.
-    if [ $c -eq $COMP_CWORD -a -z "$command" ]; then
-        COMPREPLY=($(compgen \
-            -W "--help --version copyright help $_stg_commands" \
-            -- "${COMP_WORDS[COMP_CWORD]}"))
-        return;
-    fi
-
-    # Complete arguments to subcommands.
-    case "$command" in
-        # generic commands
-        help)   _stg_help ;;
-        # repository commands
-        id)     _stg_patches $command _all_patches ;;
-        # stack commands
-        coalesce) _stg_patches $command _applied_patches ;;
-        float)  _stg_patches $command _all_patches ;;
-        goto)   _stg_patches $command _all_other_patches ;;
-        hide)   _stg_patches $command _unapplied_patches ;;
-        pop)    _stg_patches $command _applied_patches ;;
-        push)   _stg_patches $command _unapplied_patches ;;
-        series) _stg_patches $command _all_patches ;;
-        sink)   _stg_patches $command _all_patches ;;
-        unhide) _stg_patches $command _hidden_patches ;;
-        # patch commands
-        delete) _stg_patches $command _all_patches ;;
-        edit)   _stg_patches $command _applied_patches ;;
-        export) _stg_patches $command _all_patches ;;
-        files)  _stg_patches $command _all_patches ;;
-        log)    _stg_patches $command _all_patches ;;
-        mail)   _stg_patches $command _all_patches ;;
-        pick)   _stg_patches $command _unapplied_patches ;;
-#      refresh)_stg_patches_options $command _applied_patches "-p --patch" ;;
-        refresh) _complete_files $command "$(_dirty_files)" ;;
-        rename) _stg_patches $command _all_patches ;;
-        show)   _stg_patches $command _all_patches ;;
-        sync)   _stg_patches $command _applied_patches ;;
-        # working-copy commands
-        diff)   _stg_patches_options $command _applied_patches "-r --range" ;;
-       resolved) _complete_files $command "$(_conflicting_files)" ;;
-       # commands that usually raher accept branches
-       branch) _complete_branch $command _all_branches ;;
-       rebase) _complete_branch $command _all_branches ;;
-        # all the other commands
-        *)      _stg_common $command ;;
-    esac
-}
-
-complete -o default -F _stg stg
index 81854d3..fb67958 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -59,8 +59,8 @@ def __run_setup():
             ('share/stgit/examples', glob.glob('examples/*.tmpl')),
             ('share/stgit/examples', ['examples/gitconfig']),
             ('share/stgit/contrib', ['contrib/diffcol.sh',
-                                     'contrib/stgbashprompt.sh',
-                                     'contrib/stgit-completion.bash']),
+                                     'contrib/stgbashprompt.sh']),
+            ('share/stgit/completion', ['stgit-completion.bash'])
             ])
 
 # Check the minimum versions required
index 3c9dbfa..2af6523 100755 (executable)
--- a/stg-build
+++ b/stg-build
@@ -2,7 +2,7 @@
 # -*- python -*-
 import optparse, sys
 import stgit.main
-from stgit import argparse, commands
+from stgit import argparse, commands, completion
 
 def main():
     op = optparse.OptionParser()
@@ -14,6 +14,8 @@ def main():
                   help = 'Print asciidoc command list')
     op.add_option('--py-cmd-list', action = 'store_true',
                   help = 'Write Python command list')
+    op.add_option('--bash-completion', action = 'store_true',
+                  help = 'Write bash completion code')
     options, args = op.parse_args()
     if args:
         op.error('Wrong number of arguments')
@@ -30,6 +32,8 @@ def main():
     elif options.py_cmd_list:
         commands.py_commands(commands.get_commands(allow_cached = False),
                              sys.stdout)
+    elif options.bash_completion:
+        completion.write_completion(sys.stdout)
     else:
         op.error('No command')
 
index bd71817..406ada3 100644 (file)
@@ -26,28 +26,27 @@ def _paragraphs(s):
 
 class opt(object):
     """Represents a command-line flag."""
-    def __init__(self, *args, **kwargs):
-        self.args = args
+    def __init__(self, *pargs, **kwargs):
+        self.pargs = pargs
         self.kwargs = kwargs
     def get_option(self):
         kwargs = dict(self.kwargs)
         kwargs['help'] = kwargs['short']
-        del kwargs['short']
-        if 'long' in kwargs:
-            del kwargs['long']
-        return optparse.make_option(*self.args, **kwargs)
+        for k in ['short', 'long', 'args']:
+            kwargs.pop(k, None)
+        return optparse.make_option(*self.pargs, **kwargs)
     def metavar(self):
         o = self.get_option()
         if not o.nargs:
             return None
         if o.metavar:
             return o.metavar
-        for flag in self.args:
+        for flag in self.pargs:
             if flag.startswith('--'):
                 return utils.strip_prefix('--', flag).upper()
         raise Exception('Cannot determine metavar')
     def write_asciidoc(self, f):
-        for flag in self.args:
+        for flag in self.pargs:
             f.write(flag)
             m = self.metavar()
             if m:
@@ -60,6 +59,16 @@ class opt(object):
             f.write('+\n')
             for line in para:
                 f.write(line + '\n')
+    @property
+    def flags(self):
+        return self.pargs
+    @property
+    def args(self):
+        if self.kwargs.get('action', None) in ['store_true', 'store_false']:
+            default = []
+        else:
+            default = [files]
+        return self.kwargs.get('args', default)
 
 def _cmd_name(cmd_mod):
     return getattr(cmd_mod, 'name', cmd_mod.__name__.split('.')[-1])
@@ -103,11 +112,11 @@ def sign_options():
                 '--ack and --sign were both specified')
         parser.values.sign_str = sign_str
     return [
-        opt('--sign', action = 'callback', dest = 'sign_str',
+        opt('--sign', action = 'callback', dest = 'sign_str', args = [],
             callback = callback, callback_args = ('Signed-off-by',),
             short = 'Add "Signed-off-by:" line', long = """
             Add a "Signed-off-by:" to the end of the patch."""),
-        opt('--ack', action = 'callback', dest = 'sign_str',
+        opt('--ack', action = 'callback', dest = 'sign_str', args = [],
             callback = callback, callback_args = ('Acked-by',),
             short = 'Add "Acked-by:" line', long = """
             Add an "Acked-by:" line to the end of the patch.""")]
@@ -151,7 +160,7 @@ def message_options(save_template):
             callback = msg_callback, dest = 'message', type = 'string',
             short = 'Use MESSAGE instead of invoking the editor'),
         opt('-f', '--file', action = 'callback', callback = file_callback,
-            dest = 'message', type = 'string',
+            dest = 'message', type = 'string', args = [files],
             short = 'Use FILE instead of invoking the editor', long = """
             Use the contents of FILE instead of invoking the editor.
             (If FILE is "-", write to stdout.)""")]
@@ -181,6 +190,7 @@ def diff_opts_option():
             default = (config.get('stgit.diff-opts') or '').split(),
             action = 'callback', callback = diff_opts_callback,
             type = 'string', metavar = 'OPTIONS',
+            args = [strings('-M', '-C')],
             short = 'Extra options to pass to "git diff"')]
 
 def _person_opts(person, short):
@@ -212,3 +222,63 @@ def author_options():
 
 def author_committer_options():
     return _person_opts('author', 'auth') + _person_opts('committer', 'comm')
+
+class CompgenBase(object):
+    def actions(self, var): return set()
+    def words(self, var): return set()
+    def command(self, var):
+        cmd = ['compgen']
+        for act in self.actions(var):
+            cmd += ['-A', act]
+        words = self.words(var)
+        if words:
+            cmd += ['-W', '"%s"' % ' '.join(words)]
+        cmd += ['--', '"%s"' % var]
+        return ' '.join(cmd)
+
+class CompgenJoin(CompgenBase):
+    def __init__(self, a, b):
+        assert isinstance(a, CompgenBase)
+        assert isinstance(b, CompgenBase)
+        self.__a = a
+        self.__b = b
+    def words(self, var): return self.__a.words(var) | self.__b.words(var)
+    def actions(self, var): return self.__a.actions(var) | self.__b.actions(var)
+
+class Compgen(CompgenBase):
+    def __init__(self, words = frozenset(), actions = frozenset()):
+        self.__words = set(words)
+        self.__actions = set(actions)
+    def actions(self, var): return self.__actions
+    def words(self, var): return self.__words
+
+def compjoin(compgens):
+    comp = Compgen()
+    for c in compgens:
+        comp = CompgenJoin(comp, c)
+    return comp
+
+all_branches = Compgen(['$(_all_branches)'])
+stg_branches = Compgen(['$(_stg_branches)'])
+applied_patches = Compgen(['$(_applied_patches)'])
+other_applied_patches = Compgen(['$(_other_applied_patches)'])
+unapplied_patches = Compgen(['$(_unapplied_patches)'])
+hidden_patches = Compgen(['$(_hidden_patches)'])
+commit = Compgen(['$(_all_branches) $(_tags) $(_remotes)'])
+conflicting_files = Compgen(['$(_conflicting_files)'])
+dirty_files = Compgen(['$(_dirty_files)'])
+unknown_files = Compgen(['$(_unknown_files)'])
+known_files = Compgen(['$(_known_files)'])
+repo = Compgen(actions = ['directory'])
+dir = Compgen(actions = ['directory'])
+files = Compgen(actions = ['file'])
+def strings(*ss): return Compgen(ss)
+class patch_range(CompgenBase):
+    def __init__(self, *endpoints):
+        self.__endpoints = endpoints
+    def words(self, var):
+        words = set()
+        for e in self.__endpoints:
+            assert not e.actions(var)
+            words |= e.words(var)
+        return set(['$(_patch_range "%s" "%s")' % (' '.join(words), var)])
index ef71547..3d912fc 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git, basedir
+from stgit import argparse, stack, git, basedir
 from stgit.lib import log
 
 help = 'Branch operations: switch, list, create, rename, delete, ...'
@@ -45,6 +45,7 @@ within a git repository.
 'stg branch' <branch>::
         Switch to the given branch."""
 
+args = [argparse.all_branches]
 options = [
     opt('-l', '--list', action = 'store_true',
         short = 'List the branches contained in this repository', long = """
index 27a7716..9b48e7b 100644 (file)
@@ -28,6 +28,7 @@ Delete the empty patches in the whole series or only those applied or
 unapplied. A patch is considered empty if the two commit objects
 representing its boundaries refer to the same tree object."""
 
+args = []
 options = [
     opt('-a', '--applied', action = 'store_true',
         short = 'Delete the empty applied patches'),
index 659712d..efb7198 100644 (file)
@@ -18,7 +18,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 import sys, os
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Make a local clone of a remote repository'
 kind = 'repo'
@@ -35,6 +35,7 @@ commands of stglink:branch[].
 The target directory <dir> will be created by this command, and must
 not already exist."""
 
+args = [argparse.repo, argparse.dir]
 options = []
 
 directory = DirectoryAnywhere(needs_current_series = False, log = False)
index ef8e912..4b5c00a 100644 (file)
@@ -34,6 +34,8 @@ If there are conflicts when reordering the patches to match the order
 you specify, you will have to resolve them manually just as if you had
 done a sequence of pushes and pops yourself."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [opt('-n', '--name', short = 'Name of coalesced patch')
            ] + argparse.message_options(save_template = True)
 
index 99b7b5d..dd8d6e6 100644 (file)
@@ -19,6 +19,7 @@ from stgit.argparse import opt
 from stgit.commands import common
 from stgit.lib import transaction
 from stgit.out import *
+from stgit import argparse
 
 help = 'Permanently store the applied patches into the stack base'
 kind = 'stack'
@@ -40,6 +41,8 @@ The -n/--number option specifies the number of applied patches to
 commit (counting from the bottom of the stack). If -a/--all is given,
 all applied patches are committed."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
     opt('-n', '--number', type = 'int',
         short = 'Commit the specified number of patches'),
index b92a039..40cef3f 100644 (file)
@@ -19,6 +19,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 from stgit.argparse import opt
 from stgit.commands import common
 from stgit.lib import transaction
+from stgit import argparse
 
 help = 'Delete patches'
 kind = 'patch'
@@ -28,8 +29,10 @@ Delete the patches passed as arguments.
 
 Note that the 'delete' operation is irreversible."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch')]
 
 directory = common.DirectoryHasRepositoryLib()
index 05f4f4c..7d2f719 100644 (file)
@@ -38,8 +38,12 @@ representing the bottom of the current stack.
 
 rev = '[branch:](<patch>|{base}) | <tree-ish>'"""
 
+args = [argparse.known_files, argparse.dirty_files]
 options = [
     opt('-r', '--range', metavar = 'rev1[..[rev2]]', dest = 'revs',
+        args = [argparse.patch_range(argparse.applied_patches,
+                                     argparse.unapplied_patches,
+                                     argparse.hidden_patches)],
         short = 'Show the diff between revisions'),
     opt('-s', '--stat', action = 'store_true',
         short = 'Show the stat instead of the diff'),
index b370f5c..4904f68 100644 (file)
@@ -54,6 +54,8 @@ If the patch diff is edited but does not apply, no changes are made to
 the patch at all. The edited patch is saved to a file which you can
 feed to "stg edit --file", once you have made sure it does apply."""
 
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
 options = [
     opt('-d', '--diff', action = 'store_true',
         short = 'Edit the patch diff'),
index c7ed802..dfdcea1 100644 (file)
@@ -47,8 +47,11 @@ file:
   %(commname)s    - committer's name
   %(commemail)s   - committer's e-mail"""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
-    opt('-d', '--dir',
+    opt('-d', '--dir', args = [argparse.dir],
         short = 'Export patches to DIR instead of the default'),
     opt('-p', '--patch', action = 'store_true',
         short = 'Append .patch to the patch names'),
@@ -56,9 +59,9 @@ options = [
         short = 'Append .EXTENSION to the patch names'),
     opt('-n', '--numbered', action = 'store_true',
         short = 'Prefix the patch names with order numbers'),
-    opt('-t', '--template', metavar = 'FILE',
+    opt('-t', '--template', metavar = 'FILE', args = [argparse.files],
         short = 'Use FILE as a template'),
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch'),
     opt('-s', '--stdout', action = 'store_true',
         short = 'Dump the patches to the standard output'),
index d63a33e..46d43c1 100644 (file)
@@ -34,6 +34,8 @@ given patch. Note that this command doesn't show the files modified in
 the working tree and not yet included in the patch by a 'refresh'
 command. Use the 'diff' or 'status' commands for these files."""
 
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
 options = [
     opt('-s', '--stat', action = 'store_true',
         short = 'Show the diffstat'),
index 93bb69b..7c3dcdf 100644 (file)
@@ -20,7 +20,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Push patches to the top, even if applied'
 kind = 'stack'
@@ -32,6 +32,8 @@ necessary pop and push operations will be performed to accomplish
 this. The '--series' option can be used to rearrange the (top) patches
 as specified by the given series file (or the standard input)."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
     opt('-s', '--series', action = 'store_true',
         short = 'Rearrange according to a series file')]
index 165ff52..66a2dd9 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Integrate a GNU diff patch into the current patch'
 kind = 'patch'
@@ -33,10 +33,11 @@ performed with the current top. With the --base option, the patch is
 applied onto the specified base and a three-way merged is performed
 with the current top."""
 
+args = [argparse.files]
 options = [
     opt('-t', '--threeway', action = 'store_true',
         short = 'Perform a three-way merge with the current patch'),
-    opt('-b', '--base',
+    opt('-b', '--base', args = [argparse.commit],
         short = 'Use BASE instead of HEAD applying the patch')]
 
 directory = DirectoryHasRepository(log = True)
index 0d4cd29..60a917e 100644 (file)
@@ -17,6 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 from stgit.commands import common
 from stgit.lib import transaction
+from stgit import argparse
 
 help = 'Push or pop patches to the given one'
 kind = 'stack'
@@ -25,6 +26,7 @@ description = """
 Push/pop patches to/from the stack until the one given on the command
 line becomes current."""
 
+args = [argparse.other_applied_patches, argparse.unapplied_patches]
 options = []
 
 directory = common.DirectoryHasRepositoryLib()
index 1bcb5f1..014febb 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Hide a patch in the series'
 kind = 'stack'
@@ -29,8 +29,10 @@ description = """
 Hide a range of unapplied patches so that they are no longer shown in
 the plain 'series' command output."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch')]
 
 directory = DirectoryHasRepository(log = True)
index 857ec33..566edcc 100644 (file)
@@ -18,6 +18,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 from stgit.out import out
 from stgit.commands import common
 from stgit.lib import stack
+from stgit import argparse
 
 help = 'Print the git hash value of a StGit reference'
 kind = 'repo'
@@ -30,6 +31,8 @@ or the base of the stack. If no branch is specified, it defaults to the
 current one. The bottom of a patch is accessible with the
 '[<branch>:]<patch>^' format."""
 
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
 options = []
 
 directory = common.DirectoryHasRepositoryLib()
index 9f2df05..de5e9a5 100644 (file)
@@ -44,6 +44,7 @@ stack.
 The patch description has to be separated from the data with a '---'
 line."""
 
+args = [argparse.files]
 options = [
     opt('-m', '--mail', action = 'store_true',
         short = 'Import the patch from a standard e-mail file'),
@@ -61,7 +62,7 @@ options = [
         short = 'Ignore the applied patches in the series'),
     opt('--replace', action = 'store_true',
         short = 'Replace the unapplied patches in the series'),
-    opt('-b', '--base',
+    opt('-b', '--base', args = [argparse.commit],
         short = 'Use BASE instead of HEAD for file importing'),
     opt('-e', '--edit', action = 'store_true',
         short = 'Invoke an editor for the patch description'),
index 67d20d1..6ffb93e 100644 (file)
@@ -27,6 +27,7 @@ Initialise the current git branch to be used as an StGIT stack. The
 branch (and the git repository it is in) must already exist and
 contain at least one commit."""
 
+args = []
 options = []
 
 directory = common.DirectoryHasRepositoryLib()
index 1f63ef5..39cdfe7 100644 (file)
@@ -20,7 +20,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import os.path
 from optparse import make_option
-from stgit import run
+from stgit import argparse, run
 from stgit.argparse import opt
 from stgit.commands import common
 from stgit.lib import log
@@ -37,8 +37,11 @@ the named patches.
 "stg undo" and "stg redo" let you step back and forth in the patch
 stack. "stg reset" lets you go directly to any state."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default one'),
     opt('-p', '--patch', action = 'store_true',
         short = 'Show the refresh diffs'),
index e0a5521..0b3157e 100644 (file)
@@ -93,6 +93,9 @@ the following:
   %(prefix)s       - 'prefix ' string passed on the command line
   %(shortdescr)s   - the first line of the patch description"""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
     opt('-a', '--all', action = 'store_true',
         short = 'E-mail all the applied patches'),
@@ -134,7 +137,7 @@ options = [
         short = 'Password for SMTP authentication'),
     opt('-T', '--smtp-tls', action = 'store_true',
         short = 'Use SMTP with TLS encryption'),
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch'),
     opt('-m', '--mbox', action = 'store_true',
         short = 'Generate an mbox file instead of sending')
index 4117e4e..067882a 100644 (file)
@@ -39,6 +39,7 @@ the patch, unless the '--message' flag already specified one. The
 'patchdescr.tmpl' template file (if available) is used to pre-fill the
 editor."""
 
+args = []
 options = (argparse.author_committer_options()
            + argparse.message_options(save_template = True)
            + argparse.sign_options())
index e877171..54fac21 100644 (file)
@@ -21,7 +21,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Show the applied patches modifying a file'
 kind = 'stack'
@@ -32,10 +32,11 @@ it shows the patches affected by the local tree modifications. The
 '--diff' option also lists the patch log and the diff for the given
 files."""
 
+args = [argparse.known_files]
 options = [
     opt('-d', '--diff', action = 'store_true',
         short = 'Show the diff for the given files'),
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch')]
 
 directory = DirectoryHasRepository(log = False)
index e1c531d..760918b 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 from stgit.stack import Series
 
 help = 'Import a patch from a different branch or a commit object'
@@ -34,14 +34,17 @@ used as the name of the current patch. It can be overridden with the
 option. The log and author information are those of the commit
 object."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
     opt('-n', '--name',
         short = 'Use NAME as the patch name'),
-    opt('-B', '--ref-branch',
+    opt('-B', '--ref-branch', args = [argparse.stg_branches],
         short = 'Pick patches from BRANCH'),
     opt('-r', '--reverse', action = 'store_true',
         short = 'Reverse the commit object before importing'),
-    opt('-p', '--parent', metavar = 'COMMITID',
+    opt('-p', '--parent', metavar = 'COMMITID', args = [argparse.commit],
         short = 'Use COMMITID as parent'),
     opt('-x', '--expose', action = 'store_true',
         short = 'Append the imported commit id to the patch log'),
index 855dc09..2c78ac2 100644 (file)
@@ -20,7 +20,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Pop one or more patches from the stack'
 kind = 'stack'
@@ -35,6 +35,7 @@ patches passed on the command line are popped from the stack. Some of
 the push operations may fail because of conflicts ("stg undo" would
 revert the last push operation)."""
 
+args = [argparse.patch_range(argparse.applied_patches)]
 options = [
     opt('-a', '--all', action = 'store_true',
         short = 'Pop all the applied patches'),
index 82035c6..f6d1398 100644 (file)
@@ -21,7 +21,7 @@ from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
 from stgit.config import GitConfigException
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Pull changes from a remote repository'
 kind = 'stack'
@@ -37,6 +37,7 @@ resolved and the patch pushed again.
 
 Check the 'git fetch' documentation for the <repository> format."""
 
+args = [argparse.repo]
 options = [
     opt('-n', '--nopush', action = 'store_true',
         short = 'Do not push the patches back after pulling'),
index a7c7578..818e02d 100644 (file)
@@ -21,7 +21,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Push one or more patches onto the stack'
 kind = 'stack'
@@ -38,6 +38,7 @@ conflicting push with 'stg undo').
 The command also notifies when the patch becomes empty (fully merged
 upstream) or is modified (three-way merged) by the 'push' operation."""
 
+args = [argparse.patch_range(argparse.unapplied_patches)]
 options = [
     opt('-a', '--all', action = 'store_true',
         short = 'Push all the unapplied patches'),
index 60168ab..a4bc6e7 100644 (file)
@@ -19,7 +19,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Move the stack base to another point in history'
 kind = 'stack'
@@ -40,6 +40,7 @@ Or if you want to skip that patch:
         $ stg undo --hard
         $ stg push next-patch..top-patch"""
 
+args = [argparse.commit]
 options = [
     opt('-n', '--nopush', action = 'store_true',
         short = 'Do not push the patches back after rebasing'),
index 8e62a1d..eb8b20f 100644 (file)
@@ -32,6 +32,7 @@ the effects of consecutive invocations of "stg undo".
 It is an error to run "stg redo" if the last command was not an
 undo."""
 
+args = []
 options = [
     opt('-n', '--number', type = 'int', metavar = 'N', default = 1,
         short = 'Undo the last N undos'),
index 3c82906..27cccc5 100644 (file)
@@ -48,6 +48,7 @@ the patch stack log; this means that one undo step will undo the merge
 between the other patch and the temp patch, and two undo steps will
 additionally get rid of the temp patch."""
 
+args = [argparse.dirty_files]
 options = [
     opt('-u', '--update', action = 'store_true',
         short = 'Only update the current patch files'),
@@ -55,7 +56,8 @@ options = [
         short = 'Refresh from index instead of worktree', long = """
         Instead of setting the patch top to the current contents of
         the worktree, set it to the current contents of the index."""),
-    opt('-p', '--patch',
+    opt('-p', '--patch', args = [argparse.other_applied_patches,
+                                 argparse.unapplied_patches],
         short = 'Refresh (applied) PATCH instead of the top patch'),
     opt('-e', '--edit', action = 'store_true',
         short = 'Invoke an editor for the patch description'),
index 7e0fbf5..8a593ac 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Rename a patch'
 kind = 'patch'
@@ -29,8 +29,10 @@ description = """
 Rename <oldpatch> into <newpatch> in a series. If <oldpatch> is not
 given, the top-most patch will be renamed."""
 
+args = [argparse.applied_patches, argparse.unapplied_patches,
+        argparse.hidden_patches]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'use BRANCH instead of the default one')]
 
 directory = DirectoryHasRepository(log = True)
index ff9bb61..37c4bab 100644 (file)
@@ -69,6 +69,7 @@ NOTE: If using git commands on the stack was a mistake, running "stg
 repair" is _not_ what you want. In that case, what you want is option
 (1) above."""
 
+args = []
 options = []
 
 directory = DirectoryGotoToplevel(log = True)
index 3ccbf1b..7dfd4a0 100644 (file)
@@ -21,6 +21,7 @@ from stgit.argparse import opt
 from stgit.commands import common
 from stgit.lib import git, log, transaction
 from stgit.out import out
+from stgit import argparse
 
 help = 'Reset the patch stack to an earlier state'
 kind = 'stack'
@@ -33,6 +34,9 @@ a commit id from a stack log; "stg log" lets you view this log, and
 If one or more patch names are given, reset only those patches, and
 leave the rest alone."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
     opt('--hard', action = 'store_true',
         short = 'Discard changes in your index/worktree')]
index ce8630d..2ce7ec3 100644 (file)
@@ -20,7 +20,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git, basedir
+from stgit import argparse, stack, git, basedir
 from stgit.config import config, file_extensions
 from stgit.gitmergeonefile import interactive_merge
 
@@ -32,10 +32,12 @@ Mark a merge conflict as resolved. The conflicts can be seen with the
 'status' command, the corresponding files being prefixed with a
 'C'."""
 
+args = [argparse.conflicting_files]
 options = [
     opt('-a', '--all', action = 'store_true',
         short = 'Mark all conflicts as solved'),
     opt('-r', '--reset', metavar = '(ancestor|current|patched)',
+        args = [argparse.strings('ancestor', 'current', 'patched')],
         short = 'Reset the file(s) to the given state'),
     opt('-i', '--interactive', action = 'store_true',
         short = 'Run the interactive merging tool')]
index e9d148a..95196d3 100644 (file)
@@ -21,6 +21,7 @@ from stgit.commands import common
 from stgit.commands.common import parse_patches
 from stgit.out import out
 from stgit.config import config
+from stgit import argparse
 
 help = 'Print the patch series'
 kind = 'stack'
@@ -31,8 +32,11 @@ range. The applied patches are prefixed with a '+', the unapplied ones
 with a '-' and the hidden ones with a '!'. The current patch is
 prefixed with a '>'. Empty patches are prefixed with a '0'."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch'),
     opt('-a', '--all', action = 'store_true',
         short = 'Show all patches, including the hidden ones'),
@@ -42,7 +46,7 @@ options = [
         short = 'Show the unapplied patches only'),
     opt('-H', '--hidden', action = 'store_true',
         short = 'Show the hidden patches only'),
-    opt('-m', '--missing', metavar = 'BRANCH',
+    opt('-m', '--missing', metavar = 'BRANCH',  args = [argparse.stg_branches],
         short = 'Show patches in BRANCH missing in current'),
     opt('-c', '--count', action = 'store_true',
         short = 'Print the number of patches in the series'),
index e08551b..9a1f48b 100644 (file)
@@ -28,8 +28,11 @@ description = """
 Show the commit log and the diff corresponding to the given patches.
 The output is similar to that generated by 'git show'."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches,
+                             argparse.hidden_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch'),
     opt('-a', '--applied', action = 'store_true',
         short = 'Show the applied patches'),
index 34f81c9..d4561ed 100644 (file)
@@ -20,7 +20,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Send patches deeper down the stack'
 kind = 'stack'
@@ -41,12 +41,14 @@ including <target patch>), then pushing the patches to sink, and then
 (unless '--nopush' is also given) pushing back into place the
 formerly-applied patches."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
     opt('-n', '--nopush', action = 'store_true',
         short = 'Do not push the patches back after sinking', long = """
         Do not push back on the stack the formerly-applied patches.
         Only the patches to sink are pushed."""),
-    opt('-t', '--to', metavar = 'TARGET',
+    opt('-t', '--to', metavar = 'TARGET', args = [argparse.applied_patches],
         short = 'Sink patches below the TARGET patch', long = """
         Specify a target patch to place the patches below, instead of
         sinking them to the bottom of the stack.""")]
index c78bc1b..730b47c 100644 (file)
@@ -20,7 +20,7 @@ import sys, os
 from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Show the tree status'
 kind = 'wc'
@@ -39,6 +39,7 @@ under revision control. The files are prefixed as follows:
 An 'stg refresh' command clears the status of the modified, new and
 deleted files."""
 
+args = [argparse.files]
 options = [
     opt('-m', '--modified', action = 'store_true',
         short = 'Show modified files only'),
index 966ac55..ea949d6 100644 (file)
@@ -21,7 +21,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Synchronise patches with a branch or a series'
 kind = 'patch'
@@ -33,12 +33,14 @@ for keeping patches on several branches in sync. Note that the
 operation may fail for some patches because of conflicts. The patches
 in the series must apply cleanly."""
 
+args = [argparse.patch_range(argparse.applied_patches,
+                             argparse.unapplied_patches)]
 options = [
     opt('-a', '--all', action = 'store_true',
         short = 'Synchronise all the applied patches'),
-    opt('-B', '--ref-branch',
+    opt('-B', '--ref-branch', args = [argparse.stg_branches],
         short = 'Syncronise patches with BRANCH'),
-    opt('-s', '--series',
+    opt('-s', '--series', args = [argparse.files],
         short = 'Syncronise patches with SERIES')]
 
 directory = DirectoryGotoToplevel(log = True)
index 523afa4..4ec37b4 100644 (file)
@@ -19,6 +19,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 from stgit.argparse import opt
 from stgit.commands import common
 from stgit.out import out
+from stgit import argparse
 
 help = 'Print the name of the top patch'
 kind = 'stack'
@@ -26,8 +27,9 @@ usage = ['']
 description = """
 Print the name of the current (topmost) patch."""
 
+args = []
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch')]
 
 directory = common.DirectoryHasRepositoryLib()
index b9950ca..bcc8bac 100644 (file)
@@ -21,7 +21,7 @@ from stgit.argparse import opt
 from stgit.commands import common
 from stgit.lib import transaction
 from stgit.out import *
-from stgit import utils
+from stgit import argparse, utils
 
 help = 'Turn regular git commits into StGit patches'
 kind = 'stack'
@@ -49,10 +49,12 @@ given commit should be uncommitted.
 Only commits with exactly one parent can be uncommitted; in other
 words, you can't uncommit a merge."""
 
+args = []
 options = [
     opt('-n', '--number', type = 'int',
         short = 'Uncommit the specified number of commits'),
-    opt('-t', '--to', short = 'Uncommit to the specified commit'),
+    opt('-t', '--to', args = [argparse.commit],
+        short = 'Uncommit to the specified commit'),
     opt('-x', '--exclusive', action = 'store_true',
         short = 'Exclude the commit specified by the --to option')]
 
index b7b1b73..6a04363 100644 (file)
@@ -29,6 +29,7 @@ description = """
 Reset the patch stack to the previous state. Consecutive invocations
 of "stg undo" will take you ever further into the past."""
 
+args = []
 options = [
     opt('-n', '--number', type = 'int', metavar = 'N', default = 1,
         short = 'Undo the last N commands'),
index acfef29..0c0832a 100644 (file)
@@ -20,7 +20,7 @@ from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.out import *
-from stgit import stack, git
+from stgit import argparse, stack, git
 
 help = 'Unhide a hidden patch'
 kind = 'stack'
@@ -29,8 +29,9 @@ description = """
 Unhide a hidden range of patches so that they are shown in the plain
 'stg series' command output."""
 
+args = [argparse.patch_range(argparse.hidden_patches)]
 options = [
-    opt('-b', '--branch',
+    opt('-b', '--branch', args = [argparse.stg_branches],
         short = 'Use BRANCH instead of the default branch')]
 
 directory = DirectoryHasRepository(log = True)
diff --git a/stgit/completion.py b/stgit/completion.py
new file mode 100644 (file)
index 0000000..e461e3b
--- /dev/null
@@ -0,0 +1,140 @@
+import textwrap
+import stgit.commands
+from stgit import argparse
+
+def fun(name, *body):
+    return ['%s ()' % name, '{', list(body), '}']
+
+def fun_desc(name, desc, *body):
+    return ['# %s' % desc] + fun(name, *body)
+
+def flatten(stuff, sep):
+    r = stuff[0]
+    for s in stuff[1:]:
+        r.append(sep)
+        r.extend(s)
+    return r
+
+def write(f, stuff, indent = 0):
+    for s in stuff:
+        if isinstance(s, str):
+            f.write((' '*4*indent + s).rstrip() + '\n')
+        else:
+            write(f, s, indent + 1)
+
+def patch_list_fun(type):
+    return fun('_%s_patches' % type, 'local g=$(_gitdir)',
+               'test "$g" && cat "$g/patches/$(_current_branch)/%s"' % type)
+
+def file_list_fun(name, cmd):
+    return fun('_%s_files' % name, 'local g=$(_gitdir)',
+               'test "$g" && %s' % cmd)
+
+def ref_list_fun(name, prefix):
+    return fun(name, 'local g=$(_gitdir)',
+               ("test \"$g\" && git show-ref | grep ' %s/' | sed 's,.* %s/,,'"
+                % (prefix, prefix)))
+
+def util():
+    r = [fun_desc('_gitdir',
+                  "The path to .git, or empty if we're not in a repository.",
+                  'echo "$(git rev-parse --git-dir 2>/dev/null)"'),
+         fun_desc('_current_branch',
+                  "Name of the current branch, or empty if there isn't one.",
+                  'local b=$(git symbolic-ref HEAD 2>/dev/null)',
+                  'echo ${b#refs/heads/}'),
+         fun_desc('_other_applied_patches',
+                  'List of all applied patches except the current patch.',
+                  'local b=$(_current_branch)',
+                  'local g=$(_gitdir)',
+                  ('test "$g" && cat "$g/patches/$b/applied" | grep -v'
+                   ' "^$(tail -n 1 $g/patches/$b/applied 2> /dev/null)$"')),
+         fun('_patch_range', 'local patches="$1"', 'local cur="$2"',
+             'case "$cur" in', [
+                '*..*)', ['local pfx="${cur%..*}.."', 'cur="${cur#*..}"',
+                          'compgen -P "$pfx" -W "$patches" -- "$cur"', ';;'],
+                '*)', ['compgen -W "$patches" -- "$cur"', ';;']],
+             'esac'),
+         fun('_stg_branches',
+             'local g=$(_gitdir)', 'test "$g" && (cd $g/patches/ && echo *)'),
+         ref_list_fun('_all_branches', 'refs/heads'),
+         ref_list_fun('_tags', 'refs/tags'),
+         ref_list_fun('_remotes', 'refs/remotes')]
+    for type in ['applied', 'unapplied', 'hidden']:
+        r.append(patch_list_fun(type))
+    for name, cmd in [('conflicting',
+                       r"git ls-files --unmerged | sed 's/.*\t//g' | sort -u"),
+                      ('dirty', 'git diff-index --name-only HEAD'),
+                      ('unknown', 'git ls-files --others --exclude-standard'),
+                      ('known', 'git ls-files')]:
+        r.append(file_list_fun(name, cmd))
+    return flatten(r, '')
+
+def command_list(commands):
+    return ['_stg_commands="%s"\n' % ' '.join(sorted(commands.iterkeys()))]
+
+def command_fun(cmd, modname):
+    mod = stgit.commands.get_command(modname)
+    def cg(args, flags):
+        return argparse.compjoin(list(args) + [argparse.strings(*flags)]
+                                 ).command('$cur')
+    return fun(
+        '_stg_%s' % cmd,
+        'local flags="%s"' % ' '.join(sorted(
+                flag for opt in mod.options
+                for flag in opt.flags if flag.startswith('--'))),
+        'local prev="${COMP_WORDS[COMP_CWORD-1]}"',
+        'local cur="${COMP_WORDS[COMP_CWORD]}"',
+        'case "$prev" in', [
+            '%s) COMPREPLY=($(%s)) ;;' % ('|'.join(opt.flags), cg(opt.args, []))
+            for opt in mod.options if opt.args] + [
+            '*) COMPREPLY=($(%s)) ;;' % cg(mod.args, ['$flags'])],
+        'esac')
+
+def main_switch(commands):
+    return fun(
+        '_stg',
+        'local i',
+        'local c=1',
+        'local command',
+        '',
+        'while test $c -lt $COMP_CWORD; do', [
+            'if test $c == 1; then', [
+                'command="${COMP_WORDS[c]}"'],
+            'fi',
+            'c=$((++c))'],
+        'done',
+        '',
+        ('# Complete name of subcommand if the user has not finished'
+         ' typing it yet.'),
+        'if test $c -eq $COMP_CWORD -a -z "$command"; then', [
+            ('COMPREPLY=($(compgen -W "$_stg_commands" --'
+             ' "${COMP_WORDS[COMP_CWORD]}"))'),
+            'return'],
+        'fi',
+        '',
+        '# Complete arguments to subcommands.',
+        'case "$command" in', [
+            '%s) _stg_%s ;;' % (cmd, cmd)
+            for cmd in sorted(commands.iterkeys())],
+        'esac')
+
+def install():
+    return ['complete -o default -F _stg stg']
+
+def write_completion(f):
+    commands = stgit.commands.get_commands(allow_cached = False)
+    r = [["""# -*- shell-script -*-
+# bash completion script for StGit (automatically generated)
+#
+# To use these routines:
+#
+#    1. Copy this file to somewhere (e.g. ~/.stgit-completion.bash).
+#
+#    2. Add the following line to your .bashrc:
+#         . ~/.stgit-completion.bash"""]]
+    r += [util(), command_list(commands)]
+    for cmd, (modname, _, _) in sorted(commands.iteritems()):
+        r.append(command_fun(cmd, modname))
+    r += [main_switch(commands), install()]
+    write(f, flatten(r, ''))