Nov 16

A zsh prompt for Git users

After starting to use Git a few months ago, I thought it would be useful to show the branch of the current repository in my zsh prompt. I did some searching online, but I could not find an appealing solution. Everything I found was either too slow or just didn't show the correct information. So I figured I might as well just throw one together myself.

First and foremost, my prompt needed to be fast. In addition, I wanted to show as much information as possible using the fewest number of characters. As such, I decided that in addition to the current branch, I also wanted to know when the current working directory was dirty, as well as whether or not the current branch was ahead or behind of its associated remote tracking branch. This is how my prompt ended up looking:

Git prompt

In the rest of this post I will walk through exactly what needs to be done to replicate my Git prompt.

The first thing we need to do is create a ~/.zsh/functions/ directory to house any snippets of zsh code we will need. Inside that directory, create a file named update_current_git_vars. This function will be used to set a few environment variables which will make it easy to build up our prompt.

~/.zsh/functions/update_current_git_vars
unset __CURRENT_GIT_BRANCH
unset __CURRENT_GIT_BRANCH_STATUS
unset __CURRENT_GIT_BRANCH_IS_DIRTY

local st="$(git status 2>/dev/null)"
if [[ -n "$st" ]]; then
    local -a arr
    arr=(${(f)st})

    if [[ $arr[1] =~ 'Not currently on any branch.' ]]; then
        __CURRENT_GIT_BRANCH='no-branch'
    else
        __CURRENT_GIT_BRANCH="${arr[1][(w)4]}";
    fi

    if [[ $arr[2] =~ 'Your branch is' ]]; then
        if [[ $arr[2] =~ 'ahead' ]]; then
            __CURRENT_GIT_BRANCH_STATUS='ahead'
        elif [[ $arr[2] =~ 'diverged' ]]; then
            __CURRENT_GIT_BRANCH_STATUS='diverged'
        else
            __CURRENT_GIT_BRANCH_STATUS='behind'
        fi
    fi

    if [[ ! $st =~ 'nothing to commit' ]]; then
        __CURRENT_GIT_BRANCH_IS_DIRTY='1'
    fi
fi

After the execution of this script, up to three environment variables will be set:

  • $__CURRENT_GIT_BRANCH — Set to the name of the current Git branch.
  • $__CURRENT_GIT_BRANCH_STATUS — Used to signify the status of the current Git branch. In short, it will tell us if the local branch is ahead, behind, or diverged when compared to the appropriate remote branch.
  • $__CURRENT_GIT_BRANCH_IS_DIRTY — Set to 1 if the working directory is dirty.

We can now use these environment variables to put together the text for our prompt. Create a file named prompt_git_info inside of your ~/.zsh/functions/ directory and then save it with the following code:

~/.zsh/functions/prompt_git_info
if [ -n "$__CURRENT_GIT_BRANCH" ]; then
    local s="("
    s+="$__CURRENT_GIT_BRANCH"
    case "$__CURRENT_GIT_BRANCH_STATUS" in
        ahead)
        s+="↑"
        ;;
        diverged)
        s+="↕"
        ;;
        behind)
        s+="↓"
        ;;
    esac
    if [ -n "$__CURRENT_GIT_BRANCH_IS_DIRTY" ]; then
        s+="⚡"
    fi
    s+=")"
 
    printf " %s%s" "%{${fg[yellow]}%}" $s
fi

This will construct the portion of our prompt that shows the current Git repository information. It displays the name of the branch as well as a special character or two for extra information. Here are a few examples:

  1. (master) — Currently on branch master.
  2. (1.x↓) — Currently on branch 1.x, which is behind the associated remote tracking branch.
  3. (bug51↕) — Currently on branch bug51, which has diverged from its remote tracking branch.
  4. (feature3↑⚡) — Currently on branch feature3, which is ahead of its remote tracking branch. In addition, the working directory is dirty and thus has uncommitted changes.

At this point, we need to ensure that these zsh functions are executed at the appropriate times. We really do not want to run update_current_git_vars before each and every prompt. Instead, we will try to limit the number of times this code is executed. At the very least, we will need to update the prompt after changing to a new directory. We do this by creating a file named chpwd_update_git_vars in the ~/.zsh/functions/ directory. Save it with the following line of code:

~/.zsh/functions/chpwd_update_git_vars
update_current_git_vars

Later, we will configure zsh to execute this particular function each and every time the current directory is changed.

We should also run update_current_git_vars whenever a git command is executed. This way, the prompt will update itself immediately after a fetch, commit, push, or pull. Unfortunately, there is no post-execution hook in zsh. However, we can simulate this by using both the pre-execution hook as well as the pre-cmd hook. The pre-execution hook will see that we are about to run a git command and set a temporary environment variable which the pre-cmd hook will see. This second hook can then run update_current_git_vars right before the prompt is displayed.

In our ~/.zsh/functions/ directory, create a file named preexec_update_git_vars and save it with the following contents:

~/.zsh/functions/preexec_update_git_vars
case "$1" in 
    git*)
    __EXECUTED_GIT_COMMAND=1
    ;;
esac

Then, in the same directory, create a file named precmd_update_git_vars and save it with the following code:

~/.zsh/functions/precmd_update_git_vars
if [ -n "$__EXECUTED_GIT_COMMAND" ]; then
    update_current_git_vars
    unset __EXECUTED_GIT_COMMAND
fi

This should be sufficient for our needs. The main scenario that is not covered here is the dirtying of our working copy. If we edit a file with vim, TextMate, or any other editor, the prompt will not automatically update itself to display this dirty status. I think that this is acceptable, especially since it would be too difficult to determine when the working copy had been dirtied without resorting to calling update_current_git_vars before each and every prompt. That, and a simple git status or git diff will ensure that the prompt is back up-to-date again.

We are now done creating zsh functions and simply need to hook everything together. Ensure that all of the files created in ~/.zsh/functions/ are flagged as executable. Then, add the following to your .zshrc file:

~/.zshrc
# Initialize colors.
autoload -U colors
colors
 
# Allow for functions in the prompt.
setopt PROMPT_SUBST
 
# Autoload zsh functions.
fpath=(~/.zsh/functions $fpath)
autoload -U ~/.zsh/functions/*(:t)
 
# Enable auto-execution of functions.
typeset -ga preexec_functions
typeset -ga precmd_functions
typeset -ga chpwd_functions
 
# Append git functions needed for prompt.
preexec_functions+='preexec_update_git_vars'
precmd_functions+='precmd_update_git_vars'
chpwd_functions+='chpwd_update_git_vars'
 
# Set the prompt.
PROMPT=$'%{${fg[cyan]}%}%B%~%b$(prompt_git_info)%{${fg[default]}%} '

A few notes about these .zshrc entries:

  • While it is not necessary to autoload the colors module, it makes things much easier. Who would ever want to use \e[0;34m to signify blue text when instead they could type %{${fg[blue]}%}? Yeah, it's still not the most readable piece of text in the world, but at least it's a little better.
  • In my particular prompt, %B%~%b displays the current working directory in bold.
  • The Git portion of the prompt is printed in yellow. This can be easily changed by editing the end of the prompt_git_info function we created earlier.
  • Finally, we use the colors module again to set the text color back to normal with %{${fg[default]}%}. This is just a nice way to default the prompt back to however the user has configured their terminal.

And that's all there is to it! It's quite a bit of code, but I am happy with the way it turned out. I would hardly consider myself an expert in shell scripting, so if you see any ways in which my scripts can be improved, please leave a comment!

You can view and download all of this code at once in this gist.