Show Git information in your prompt

I’ve been a sworn fan of version control for a good few years now. After a brief flirtation with Subversion I am currently in a long term and very committed relationship with the Git version control system. I use Git to store all my code and writing and to keep everything in sync between my machines. Almost everything I do goes into a repository.

When I’m working I spend most of my time in three applications: a text editor (generally Emacs), a terminal (either iTerm2 or Gnome Terminal) and a browser (Firefox or Safari). When in Emacs I use the excellent Magit mode to keep track of the status of my current project repository. However my interaction with git is generally split between Emacs and the terminal. There’s no real pattern, just what’s easiest and open at the moment. Unfortunately when I’m in the terminal there’s no visible cue as to what the status of the repo is. I have to be careful to run git status regularly to see what’s going. I need to manually make sure that I’ve committed everything and pushed to the remote server. Though this isn’t usually a problem, every now and then I’ll forget to commit and push something on one of my machines, go to another and then realized I’ve left behind all my work. It’s annoying and kills productivity.

Over the last few days I decided to sit down and give my terminal a regular indicator of the state of the current repository. So without further ado, here’s how I altered my Bash prompt to show relevant Git information.

Extracting Git information

There are generally three things I’m concerned about when it comes the Git repo I’m currently working on:

  1. What is the current branch I’m on?
  2. Are there any changes that haven’t been committed?
  3. Are there local commits that haven’t been pushed upstream?

Git provides a number of tools that gives you a lot of very detailed information about the state of the repo. Those tools are just a few commands away and I don’t want to be seeing everything there is to be seen at every step. I just want the minimum information to answer the above question.

Since the bash prompt is always visible (and updated after each command) I can put a small amount of text in the prompt to give me the information I want. In particular my prompt should show:

  1. The name of the current branch
  2. A “dirty” indicator if there are files that have been changed but not committed
  3. The number of local commits that haven’t been pushed

What is the current branch?

The symbolic-ref command shows the branch that the given reference points to. Since HEAD is the symbolic reference for the current state of the working tree, we can use git symbolic-ref HEAD to get the full branch. If we were on the master branch we would get back something like refs/heads/master. We use a little Awk magic to get rid of everything but the part after the last /. Wrapping this into a litte function we get:


function git-branch-name
{
    echo $(git symbolic-ref HEAD 2>/dev/null | awk -F/ {'print $NF'})
}

Has everything been committed?

Next we want to know if the branch is dirty, i.e. if there are uncommitted changes. The git status command gives us a detailed listing of the state of the repo. For our purposes is the very last line of the output. If there are no outstanding changes it says “nothing to commit (working directory clean)”. We can isolate the last line using the Unix tail utility and if it doesn’t match the above message we print a small asterisk (*). This is just enough to tell us that there is something we need to know about the repo and should run the full git status command.

Again, wrapping this all up into a little function we have:

function git-dirty {
    st=$(git status 2>/dev/null | tail -n 1)
    if [[ $st != "nothing to commit (working directory clean)" ]]
    then
        echo "*"
    fi
}

Have all commits been pushed?

Finally we want to know if all commits to the respective remote branch. We can use the git branch -v command to get a verbose listing of all the local branches. Since we already know the name of the branch we’re on, we use grep to isolate the line that tells us about our branch of interest. If we have local commits that haven’t been pushed the status line will say something like “[ahead X]”, where X is the number of commits not pushed. We want to get that number.

Since what we’re looking for is a very well-defined pattern I decided to use BASH’s built-in regular expressions. I provide a pattern that matches =”[ahead X]” where X is a number. The matching number is stored in the BASH_REMATCH array. I can then print the number or nothing if no such match is present in the status line. The function we get is this:

function git-unpushed {
    brinfo=$(git branch -v | grep git-branch-name)
    if [[ $brinfo =~ ("[ahead "([[:digit:]]*)]) ]]
    then
        echo "(${BASH_REMATCH[2]})"
    fi
}

The =~ is the BASH regex match operator and the pattern used follows it.

Assembling the prompt

All that’s left is to tie together the functions and have them show up in the BASH prompt. I used a little function to check if the current directory is actually part of a repo. If the =git status= command only returns an error and nothing else then I’m not in a git repo and the functions I made would only give nonsense results. This functions checks the =git status= and then calls the other functions or does nothing.

function gitify {
    status=$(git status 2>/dev/null | tail -n 1)
    if [[ $status == "" ]]
    then
        echo ""
    else
        echo $(git-branch-name)$(git-dirty)$(git-unpushed)
    fi
}

Finally we could put together prompt. BASH allows for some common system information to be displayed in the prompt. I like to see the current hostname (to know which machine I’m on if I’m working over SSH) and the path to the directory I’m in. That’s what the \h and the \w are for. The Git information comes after that (if there is any) followed by a >. I also like to make use of BASH’s color support.

function make-prompt
{
    local RED="\[033[0;31m\]"
    local GREEN="\[033[0;32m\]"
    local LIGHT_GRAY="\[033[0;37m\]"
    local CYAN="\[033[0;36m\]"

    PS1="${CYAN}\h\
${GREEN} \w\
${RED} \$(gitify)\
${GREEN} >\
${LIGHT_GRAY} "

}

Conclusion

I like this prompt because it gives me just enough information at a glance. I know where I am, if any changes have been made and how much I’ve diverged from the remote copy of my work. When I’m not in a Git repo the git information is gone. It’s clean simple and informative.

I’ve borrowed heavily from both Jon Maddox and Zach Holman for some of the functionality. I didn’t come across anyone showing the commit count, but I wouldn’t be surprised if lots of other people have it too. There are probably other ways to get the same effect, this is just what I’ve found and settled on. The whole setup is available as a gist so feel free to use or fork it.

Shell scripting: the silent hero

In 5+ years of using Linux I’ve never really taken the time to learn shell scripting. A combination of using mostly graphical tools, Emacs and other scripting languages means that I’ve rarely found myself in a position where I’ve had to depend on a shell script. In fact the most I’ve done is writing short aliases for longer commands I use on a regular basis. When I needed to do automation I generally fell back on Python (and more recently Ruby). I prefer using a small, uniform set of tools so I tried to do as much as I could within a bunch of Python scripts.

I’ve been doing some data collection work for my thesis and normally I would have been perfectly fine using Ruby and Rake for automation. Rake is Ruby task runner, similar to good old Make but with all the power of Ruby behind it. But I needed to run the same set of tasks under different Ruby VMs (specifically MRI and JRuby). So some of the automation needed to be outside of Ruby. I currently have a shell script that uses RVM to set the Ruby VM, runs a set of Rake tasks, changes over to another VM and repeats the Rake tasks. Each VM sends output to a different file. In between each iteration of Rake tasks, the shell script drops in the iteration number into the output file for that VM. I could have had this happen inside Ruby by passing the iteration number to the Rake task, but it’s easier to put it in from the shell script.

I’m using the BASH shell, mostly because it’s what comes default on Linux boxes, but I think what I’m going to say will be true of other shells like zsh as well. Shell scripting is a powerful tool for lots of reasons. Firstly the scripting language itself is pretty full featured. It may not be as clean and “batteries included” as Python or Ruby, but there’s enough there to get a lot done. Variables, conditionals, loops and functions are all there and the syntax is pretty relaxed compared to some languages (especially when it comes to interpolating strings or commands with variables). BASH also plays to the strengths of the Unix philosophy. It works well with the small, powerful UNIX utility programs and the idea that “everything is a file”. Being able to directly run programs and then pipe the results around is an extremely flexible way to get things done. IO redirection is also very powerful, especially if you’re working with files (like I am).

Perhaps the best thing about Bash scripting (that has been inherited by Perl, Python, Ruby and the rest) is that you can get stuff done with very little knowledge of shell scripting. It’s just as easy to pick up new tricks and tools as you go along. Personally, though I had only used aliases for years I was able to move up to functions, loops and conditionals in just a few hours. I wouldn’t say I’m a proficient shell scripter, but I could certainly hold my own against some ugly automation task at this point. Admittedly having a few years experience in similar languages helped as I knew what to look for.

I’m very glad I got a chance to add shell scripting to my personal arsenal of programming tools. For me, I’m not going to sit down with a book to study shell scripting in any formal way. I’ll keep my current knowledge tucked away in my brain for future reference and pulling it out (maybe adding to it) when I need to. It’s definitely a learn-as-you-go skill for me, but one that I’m glad to have.