ZSH autocompletion for git unstage

I recently switched to git as my main version control system (I used to use Mercurial before that). I quickly grasped the concept of the staging area and used the git add command extensively to add before committing.

And zsh ships with nice git autocompletion features, and suggests files to add with git add when you press Tab.

Git unstage

What is missing from the basic git commands is a way to unstage a file from the staging area. Well, I can do it with git reset HEAD, but this command is a bit tedious to type.

So I created an alias, named git unstage that does just that. I just added the following lines to my ~/.gitconfig, under the [alias] header :

unstage = reset HEAD

I can now easily add files to the staging area with git add and remove them with git unstage


At this point, git unstage autocompletion does not work. It simply suggest all files in the current directory, while I'd like it to suggest only files in the staging area.

When you create a git alias, you cannot simply add a zsh autocomplete method for that alias (meaning, you cannot create a _git-unstage method), you have to hook your custom autocomplete logic to the underlying git method your alias refers to. In this case, this is the git reset method.

So, I created my own _git-reset autocompletion function. Actually, I borrowed the one already defined in /usr/share/zsh/functions/Completion/Unix/_git and tweaked it a little bit.

I created a file named _git-reset and put it in my $FPATH so zsh will load it when asked for autocompletion and it will overwrite the _git-reset method already defined in _git.

Here is the full content of the file :

#compdef git-reset

_git-reset () {
    local curcontext=$curcontext state line
    typeset -A opt_args

    _arguments -C -S -A '-*' \
            '(-q --quiet)'{-q,--quiet}'[be quiet, only report errors]' \
            '::commit:__git_revisions' \
        - reset-head \
            '(        --soft --hard --merge --keep)--mixed[reset the index but not the working tree (default)]' \
            '(--mixed        --hard --merge --keep)--soft[do not touch the index file nor the working tree]' \
            '(--mixed --soft        --merge --keep)--hard[match the working tree and index to the given tree]' \
            '(--mixed --soft --hard         --keep)--merge[reset out of a conflicted merge]' \
            '(--mixed --soft --hard --merge       )--keep[like --hard, but keep local working tree changes]' \
        - reset-paths \
            '(-p --patch)'{-p,--patch}'[select diff hunks to remove from the index]' \
            '*::file:->files' && ret=0

    case $state in
            local commit
            if [[ -n $line[1] ]] && __git_is_committish $line[1]; then
            # Suggest files in index if `git reset HEAD`
            if [[ $line[1] = HEAD ]]; then
                __git_tree_files . $commit

_git-reset "$@"

As you may have noticed this script is an almost exact copy/paste from the original _git-reset script. The only modification I've done is in those lines :

# Suggest files in index if `git reset HEAD`
if [[ $line[1] = HEAD ]]; then
    __git_tree_files . $commit

What it does is checking the first argument of git reset, and if it's HEAD, it suggests files in the staging area (__git_changed_files) instead of files in the current repo (__git_tree_files).

It took me quite a bit of time to figure out which method to use to get files in the staging area as I had been looking for a __git_staged_files for quite a while and finally discovered that the __git_changed_files was actually what I was looking for.


The git unstage alias is quite common, you can find it in a lot of books and websites teaching git. But it becomes much more usable once zsh does the autocomplete for you and you can easily select files to unstage that way.

Tags : #autocomplete, #git, #zsh, #unstage

Want to add something ? Feel free to get in touch on Twitter : @pixelastic