Search code examples
bashgitautocompletetab-completion

How can I make tab completion for my Git alias behave like an existing command?


I am writing a shell script to add a "review" sub command to git, to make it easier to do code reviews. It's basically a fancy wrapper for git diff and git difftool commands, but requires less typing.

Some example usages:

# Lists added, deleted, renamed and modified files between master and current branch
git review master -l

# Opens difftool for files modified between master and current branch
git review -m

I would like to enable branch auto completion in my shell script, but cannot for the life of me figure out how to do it. Here is what I'm really after:

git review ma<tab><tab>

And then it should behave like git checkout ma<tab><tab>.

My shell script:

#!/bin/env bash

function print_usage {
    echo "Usage: git review <branch> <-a|-d|-m|-l> [-- paths/to filter/by]"
    echo ""
    echo "    -a: Only review added files"
    echo "    -d: Only review delete files"
    echo "    -m: Only review modified files"
    echo "    -l: List files and and type of modification"
}

if [ -z "$1" ] || [ -z "$2" ]; then
    print_usage
    exit 1
fi

git_branch=$1
review_command=$2
path_filters=""

shift
shift

if [ "$1" = "--" ]; then
    path_filters="$@"
fi

case $review_command in
    "-a")
        echo "Reviewing added files..."

        if [ -z "$path_filters" ]; then
            git difftool $git_branch -- $(git diff --name-status $git_branch | grep -E '^A' | awk '{print $2}')
        else
            git difftool $git_branch -- $(git diff --name-status $git_branch -- $path_filters | grep -E '^A' | awk '{print $2}')
        fi
        ;;
    "-d")
        echo "Reviewing deleted files..."

        if [ -z "$path_filters" ]; then
            git difftool $git_branch -- $(git diff --name-status $git_branch | grep -E '^D' | awk '{print $2}')
        else
            git difftool $git_branch -- $(git diff --name-status $git_branch -- $path_filters | grep -E '^D' | awk '{print $2}')
        fi
        ;;
    "-m")
        echo "Reviewing modified files..."

        if [ -z "$path_filters" ]; then
            git difftool $git_branch -- $(git diff --name-status $git_branch | grep -E '^M' | awk '{print $2}')
        else
            git difftool $git_branch -- $(git diff --name-status $git_branch -- $path_filters | grep -E '^M' | awk '{print $2}')
        fi
        ;;
    "-l")
        echo "Differences between $git_branch and $(git mybranch)..."

        if [ -z "$path_filters" ]; then
            git diff --name-status $git_branch
        else
            git diff --name-status $git_branch -- $path_filters
        fi
        ;;
esac

echo ""
echo "Review finished."

Really, since I'm in the process of typing the command in, I doubt my shell script will have anything to do with the solution.

Some other useful info:

  • Windows 10
  • Git v2.18.0.windows.1
  • Shell: GNU bash, version 4.4.19(2)-release (x86_64-pc-msys)

Is there a way to add git branch auto completion to a custom git extension command?


An interesting related question for Linux: custom git command autocompletion.


Solution

  • Using an alias

    Let's assume a minimal version of your script, something like

    #!/usr/bin/env bash
    
    git diff "$@"
    

    This is executable and somewhere in your $PATH. Let's call it gitreview.

    To get an alias for it, you add it like this in your .gitconfig:

    [alias]
        review = "!f() { gitreview \"$@\"; }; f"
    

    This gets you completion for review when you enter git. Now, to make it behave the same as git checkout, you can use a null command, like this:

    review = "!f() { : git checkout ; gitreview \"$@\"; }; f"
    

    and it'll complete the same as git checkout! Notice that the blank between checkout and ; is required.


    This is mentioned in the comments of git-completion.bash:

    # If you use complex aliases of form '!f() { ... }; f', you can use the null
    # command ':' as the first command in the function body to declare the desired
    # completion style.  For example '!f() { : git commit ; ... }; f' will
    # tell the completion to use commit completion.  This also works with aliases
    # of form "!sh -c '...'".  For example, "!sh -c ': git commit ; ... '".
    

    Using an external command

    Alternatively, you can make your script a proper external command by naming it git-review and leave it somewhere in your $PATH. To get completion for it (assuming you have both Bash and Git completion), add this to your ~/.bash_completion file (just create it if it doesn't exist):

    _git_review() { _git_checkout; }
    

    Git completion (for Git 2.18 or newer) checks for completion functions that are named _git_$command, and by doing this, you say "just call _git_checkout instead".