Adding some VCS information in bash prompt

I don't spend a lot of time customizing my "working" environment nowadays, like enhancing vim configuration, or tweaking the shell. But when I read MadCoder's zsh git-enabled prompt, I though it was too convenient to not have something like that. Except I don't work with git only (sadly, but that's changing), and I don't like colours in prompt (and a 2 lines prompt is too much).

Anyways, since I have a bunch of directories in my $HOME that contain either svk, svn, mercurial, or git working trees, I thought it would be nice to have some information about all this on my prompt.

After a few iterations, here are the sample results:

mh@namakemono:~/dd/packages$
mh@namakemono:(svn)~/dd/packages/iceape[trunk:39972]debian/patches$
mh@namakemono:(svk)~/dd/packages/libxml2[trunk:1308]include/libxml$
mh@namakemono:(hg)~/moz/cvs-trunk-mirror[default]uriloader/exthandler$
mh@namakemono:(git)~/git/webkit[debian]JavaScriptCore/wtf$

The script follows, with a bit of explanation intertwined.

_bold=$(tput bold)
_normal=$(tput sgr0)

tput is a tool I only dicovered recently, and avoids the need to know the escape codes. There are also options for cursor placement, colours, etc. It lies in the ncurses-bin package, if you want to play with it.

__vcs_dir() {
  local vcs base_dir sub_dir ref
  sub_dir() {
    local sub_dir
    sub_dir=$(readlink -f "${PWD}")
    sub_dir=${sub_dir#$1}
    echo ${sub_dir#/}
  }

We declare as much as possible as local (even functions), so that we avoid cluttering the whole environment. sub_dir is going to be used in several places below, which is why we declare it as a function. It outputs the current directory, relative to the directory given as argument.

  git_dir() {
    base_dir=$(git-rev-parse --show-cdup 2>/dev/null) || return 1
    base_dir=$(readlink -f "$base_dir/..")
    sub_dir=$(git-rev-parse --show-prefix)
    sub_dir=${sub_dir%/}
    ref=$(git-symbolic-ref -q HEAD || git-name-rev --name-only HEAD 2>/dev/null)
    ref=${ref#refs/heads/}
    vcs="git"
  }

This is the first function to detect a working tree, for git this time. Each of these functions set the 4 variables we declared earlier: vcs, base_dir, sub_dir and ref. They are, respectively, the VCS type, the top-level directory of the working tree, the current directory, relative to base_dir, and the branch, revision or a reference in the repository, depending on the VCS in use. These functions return 1 if the current directory is not in a working tree of the currently considered VCS.
The base directory of a git working tree can be deduced from the result of git-rev-parse --show-cdup, which gives the way up to the top-level directory, relative to the current directory. readlink -f then gives the canonical top-level directory. The current directory, relative to the top-level, is simply given by git-rev-parse --show-prefix.
git-name-rev --name-only HEAD gives a nice reference for the current HEAD, especially if you're on a detached head. But this can turn out to do a lot of work, introducing a slight lag when you cd for the first time in the git working tree, while most of the time, the HEAD is just a symbolic ref. This is why we first try git-symbolic-ref --name-only HEAD.

  svn_dir() {
    [ -d ".svn" ] || return 1
    base_dir="."
    while [ -d "$base_dir/../.svn" ]; do base_dir="$base_dir/.."; done
    base_dir=$(readlink -f "$base_dir")
    sub_dir=$(sub_dir "${base_dir}")
    ref=$(svn info "$base_dir" | awk '/^URL/ { sub(".*/","",$0); r=$0 } /^Revision/ { sub("[^0-9]*","",$0); print r":"$0 }')
    vcs="svn"
  }

Detecting an svn working tree is easier : it contains a .svn directory, be it top-level or sub directory. We look up the top-level directory by checking the last directory containing a .svn sub directory on the way up. This obviously doesn't work if you checkout under another svn working tree, but I don't do such things.
For the ref, I wanted something like the name of the directory that has been checked out at the top-level directory (usually "trunk" or a branch name), followed by the revision number.

  svk_dir() {
    [ -f ~/.svk/config ] || return 1
    base_dir=$(awk '/: *$/ { sub(/^ */,"",$0); sub(/: *$/,"",$0); if (match("'${PWD}'", $0"(/|$)")) { print $0; d=1; } } /depotpath/ && d == 1 { sub(".*/","",$0); r=$0 } /revision/ && d == 1 { print r ":" $2; exit 1 }' ~/.svk/config) && return 1
    ref=${base_dir##*
}
    base_dir=${base_dir%%
*}
    sub_dir=$(sub_dir "${base_dir}")
    vcs="svk"
  }

svk doesn't have repository files in the working tree, so we would have to ask svk itself if the current directory is a working tree. Unfortunately, svk is quite slow at that (not that it takes several seconds, but that induces a noticeable delay to display the prompt), so we have to parse its config file by ourselves. We avoid running awk twice by outputing both the informations we are looking for, separated by a carriage return, and then do some tricks with bash variable expansion.

  hg_dir() {
    base_dir="."
    while [ ! -d "$base_dir/.hg" ]; do base_dir="$base_dir/.."; [ $(readlink -f "${base_dir}") = "/" ] && return 1; done
    base_dir=$(readlink -f "$base_dir")
    sub_dir=$(sub_dir "${base_dir}")
    ref=$(< "${base_dir}/.hg/branch")
    vcs="hg"
  }

I don't use mercurial much, but I happen to have exactly one working tree (a clone of http://hg.mozilla.org/cvs-trunk-mirror/), so I got some basic information. There is no way we can ask mercurial itself for information, it is too slow for that (main culprit being the python interpreter startup), so we take the informations we can (and since I don't know much about mercurial, that's really basic). Note that if you're deep in the VFS tree, but not in a mercurial working tree, the while loop may be slow. I didn't bother much looking for a better solution.

  git_dir ||
  svn_dir ||
  svk_dir ||
  hg_dir ||
  base_dir="$PWD"

Here we just run all these functions one by one, stopping at the first that matches. Adding some more for other VCS would be easy.

  echo "${vcs:+($vcs)}${_bold}${base_dir/$HOME/~}${_normal}${vcs:+[$ref]${_bold}${sub_dir}${_normal}}"
}
PS1='${debian_chroot:+($debian_chroot)}\u@\h:$(___vcs_dir)\$ '

Finally, we set up the prompt, so that it looks nice with all the gathered information.

Update: made the last lines of the script a little better and factorized.

2007-10-14 13:24:44+0900

miscellaneous, p.d.o

Both comments and pings are currently closed.

2 Responses to “Adding some VCS information in bash prompt”

  1. Ben Hutchings Says:

    bash miscalculates the length of a prompt if it includes non-printing escape sequences, and then makes a mess of wrapping input lines. You have to put \[ and \] around those sequences. I ended up with:

    __vcs_dir() {

    if [ -n “$vcs” ]; then
    __vcs_prefix=”($vcs)”
    __vcs_base_dir=”${base_dir/$HOME/~}”
    __vcs_ref=”[$ref]”
    __vcs_sub_dir=”${sub_dir}”
    else
    __vcs_prefix=”
    __vcs_base_dir=”${PWD/$HOME/~}”
    __vcs_ref=”
    __vcs_sub_dir=”
    fi
    }

    PROMPT_COMMAND=__vcs_dir
    PS1=’${debian_chroot:+($debian_chroot)}\u@\h:$__vcs_prefix\[${_bold}\]${__vcs_base_dir}\[${_normal}\]${__vcs_ref}\[${_bold}\]${__vcs_sub_dir}\[${_normal}\]\$

  2. Yaroslav Halchenko Says:

    Is your .bashrc_vcs (how I would name it before sourcing from .bash_profile) available entirely in 1 file somewhere on the web? or may be even better — from some vcs?