AaronCrane.co.uk

Showing your Git branch in your shell prompt

One of the great things about Git is that it makes branching and merging so simple as to be a delight, not a chore. One of the consequences of that is that you tend to use far more branches than you would if you were using a lesser revision-control system. Which can make it easy to forget which branch you’re on at any given moment. So wouldn’t it be nice if you had a simple, easy-to-see reminder of where you are, visible at all times?

The obvious place to put such a thing (for suitable values of “obvious”) is your shell prompt — it’s always in front of you, and as long as your branch names aren’t excessively long, there’s very little cost in putting it there. It also works correctly as you cd around your filesystem, even across multiple terminal windows or tabs.

Until today, my shell prompts have, for many years, looked like this:

aaron@sultan:~/work$ 

That is: my username, an @ sign, the current host name, a colon, the current directory, and a dollar sign. I want to add the current Git branch name, if there is one, before the dollar sign, with a space in front of it. So if I’m trying to hack on Rakudo, I should see output like this:

aaron@sultan:~$ cd work/rakudo
aaron@sultan:~/work/rakudo master$ git status
# On branch master
nothing to commit (working directory clean)
aaron@sultan:~/work/rakudo master$ git checkout range_to_setting
Switched to branch "range_to_setting"
aaron@sultan:~/work/rakudo range_to_setting$ cd
aaron@sultan:~$ 

with the indicated branch changing instantly as I switch branches or move around my filesystem.

I don’t think I’m the first person to come up with this idea, by the way; a little Googling reveals things like this blog entry by Jon Maddox. But I’ve written my own anyway. One, it was a fun little thing to work on; and two, my version is surely going to be better, right?. In particular, Jon’s implementation is built on this code:

git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'

That is, this has to fork and exec not one but two processes each time the shell emits a prompt. That seems suboptimal to me. So step 1 is to replicate the work of git branch (and Jon’s sed script) in shell. That’s not too difficult, fortunately; Git has a very simple model for where such data is stored. The .git directory at the root of your working tree contains a file named HEAD which contains one of two things: either a commit ID (if the current head is detached), or a line like this:

ref: refs/heads/branch-name

So, here’s a Bash function which first looks upwards from the current directory to find a .git directory (or bails if there isn’t one to be found), and then parses out the branch name from .git/HEAD:

function find_git_branch {
    local dir=. head
    until [ "$dir" -ef / ]; do
        if [ -f "$dir/.git/HEAD" ]; then
            head=$(< "$dir/.git/HEAD")
            if [[ $head == ref:\ refs/heads/* ]]; then
                git_branch=" ${head#*/*/}"
            elif [[ $head != '' ]]; then
                git_branch=' (detached)'
            else
                git_branch=' (unknown)'
            fi
            return
        fi
        dir="../$dir"
    done
    git_branch=''
}

There are a few subtleties here.

  1. I’m careful to rely on builtin shell capabilities for everything: reading the file, checking whether I’ve run out of directories to examine, string manipulation, you name it.

  2. I also account for the case in which I’m wrong about the format of .git/HEAD, and I can’t determine the branch name; in that situation, I emit unknown.

  3. The variable I put the branch name into begins with a space, or is completely empty if there’s no branch; that means that the variable is usable unchanged in my prompt.

  4. Most importantly: I store the branch name in a variable, rather than emitting it to stdout. The big advantage of that is that the prompt won’t have to use command interpolation (backticks) to embed the branch into the prompt. There’d be little point in writing my own branch-finding code if I needed to use backticks to get at it, since executing backticks (even for a shell function) involves piping from a forked clone of the current shell.

Having done that, the rest is relatively straightforward. First, I need to ensure the variable is set to the right value immediately before Bash constructs the prompt. That can easily be done with $PROMPT_COMMAND, which is expected to contain a shell command that is executed at the right time:

PROMPT_COMMAND="find_git_branch; $PROMPT_COMMAND"

I extend the current value rather than simply setting $PROMPT_COMMAND to find_git_branch because there are other things I do in my $PROMPT_COMMAND.

Finally, I make sure $PS1 (the variable that contains the main interactive prompt) will emit the correctly-set $git_branch variable:

green=$'\e[1;32m'
magenta=$'\e[1;35m'
normal_colours=$'\e[m'

PS1="\[$green\]\u@\h:\w\[$magenta\]\$git_branch\[$green\]\\$\[$normal_colours\] "

There are a couple of important things there, too.

  1. The $git_branch variable is protected with a backslash; that ensures it gets interpolated while Bash is generating the prompt, rather than when the variable is initialised.

  2. I directly use the standard-ish ECMA-48-derived escape sequences to colour my prompt. In theory, that’s not a very portable way of achieving that, but in practice, I’ve never used a terminal (or terminal emulator) where it doesn’t work, and I doubt I ever will.

  3. The colour-changing sequences are enclosed within \[...\]; this tells Bash not to count them towards the rendered width of the prompt. Without that, Bash would not be able to make an accurate decision about when to break the line if the prompt and/or command are too wide for the terminal.

And that’s it. Feel free to use this code unchanged, or to adapt it for your own needs. All you need to do is drop it into your ~/.bashrc (or ~/.bash_profile if you have one of those.) Share and enjoy!

Update: fixed the code to handle branch names containing a slash.