I recently read an inspiring short post named Update your Mac Terminal to display your current git branch and status which shows how to change the prompt of the terminal to display the name of the git branch and its status, if you are in a git repository. I had been thinking about such a helper for some time, but I never had really time to dedicate specifically to it so that that post actually arrived on purpose. However, it didn’t fit my requirements so this short note is about an alternative on Linux.

Introduction

If I work in the ~/tmp directory, my primary prompt is

~/tmp
maurycy@kali> 

which is the result of the following configuration in my ~/.bashrc file:

export PS1="\w\n\u@\h> "

where

  • \w displays the working directory, here ~
  • \u is the user name, maurycy, and
  • \h is the hostname, here kali

The objective is to change the prompt so that if the working directory is a git repository, it will display the current branch and a short status (if there are files that had been added, removed, etc). Cherry on the cake, let’s have the branch name written in red if there are some pending commits to be done or in green if all is clean. The result is shown on the screenshot below:

alt text

How to modify the shell prompt dynamically

Changing the prompt is a matter of modifying the PS1 bash environment variable which is displayed every time the primary prompt is to be displayed. So:

maurycy>            # current prompt
PS1="> "
>                   # new prompt

Now, if we want a dynamic content, that is a content that changes every time the prompt is displayed, we have to replace the
"> " by the output of a program. For example, imagine we want the current time as a prompt, we can use the date program with the following parameter date +"%H:%M:%S". An important thing is that as we need PS1 to be expanded every time it is displayed we need to use single quotes instead of double quotes. Putting all that together, we get:

>                                     # The prompt is just '> '
> PS1='$(date +"%H:%M:%S")>  '
09:41:40>                             # The new prompt that ...
09:41:41>                             # ... changes every time it's displyed
09:41:42>

One last point is that we also can use shell functions. For example:

> function get_date () {              # define a function
>     date +"%H:%M:%S"
> }
> PS1='$(get_date)> '                 # invoke that function
14:23:29> 
14:23:30> 

So, now that we know how to change the prompt every time it is displayed, the only thing that remains to be done is to display the status of the git repository if the current directory is one.

Getting the status of a git repository

To get the status of a git repository, we can call the git status command. However, the output is quite verbose and cumbersome to parse. Hopefully, git also proposes the --porcelain parameter which purpose, according to the man page, is to be simple to parse and to remain stable across versions. Perfect! An other interesting parameter is -b which prints the name of the current branch. In a repository with only two freshly created files, we get:

maurycy@kali> LANGUAGE=en git status -b --porcelain
## master
?? f1
?? f2
~/tmp/demorepo

If your workstation is configured in a language different from English, as mine in French, LANGUAGE=en ensures everything will be always written in English.

The leading ?? defines the status of each file. More generally, the status is a XY string X and Y can take the following values:

  • ' ' (space) : unmodified
  • ? : untracked
  • M : modified
  • A : added
  • D : deleted
  • R : renamed
  • C : copied
  • U : updated but unmerged

The meaning of the couple XY is described in the man page, but basically X represents the status of the index and Y the status of the working tree unless there is a merge conflict in which case X and Y represent the status of each side. As our goal is not to provide the full information but just an insight of the changes that had happened in the repository, we’ll only take into account the Y part.

Calculating the prompt

Now we have all the building blocks to define our “git prompt”.

We will define a shell function, get_git_status(), that will execute the command LANGUAGE=en git status -b --porcelain 2>&1 and pipe the output into an awk script which will parse it and emit a string representing the status.

There are three possible states:

  1. We are not in a repository: If the current directory is not a repository, git status ... prints a line stating with fatal on stderr; this is the reason why we redirect stderr into stdout with 2>&1 above.

  2. We are in a git repository and there are non-committed modifications : In this case git status ... output has the branch name on the first line and, then, for every non-committed file, it prints a line with the status XY as explained above. We will concatenate the Y part into one string. At the end of the process, if the calculated status is non empty, it will be printed in red.

  3. We are in a git repository and all is clean: In this case git status ... just prints one line with the branch name. The calculated status will be empty, so it will be printed in green as there is no pending commit.

To print strings in red or green we’ll use the ANSI escape sequences. They are of the form ESC[<color code>m. For example in the string "\033[31m", \033 is the escape character in octal and the code 31 sets the foreground to red. ANSI escape sequences are fully described on Wikipedia; see more specifically the SGR section and following.

Putting all this together, our get_git_status() function looks like this:

function get_git_status () {
    LANGUAGE=en git status -b --porcelain 2>&1 | awk '
    BEGIN {
      branch_name = "" 
      not_in_git = 0	   # by default we are in a git repo
      status = ""
    }

    {
      if($1=="fatal:") {   # in git repo
        not_in_git = 1
        exit
      }
      if($1=="##") {       # format is : ## <branch name>
        branch_name = $2
    	next
      }

      if(length($1) == 2)  # Get the Y of the 'XY' status
        car = substr($1, 1, 1)
      else
        car = substr($1, 0, 1)

      if(car=="?" || car=="M" || car=="A" || car =="D" || 
         car=="R" || car=="C" || car=="U") {
        status = status car
    	next
      }
    }

    END {
      s = ""
      if(not_in_git == 0) {
        if(length(status) > 0)
      	  s = " (\033[31m"branch_name" "status"\033[39m)"
        else
      	  s = " (\033[32m"branch_name"\033[39m)"
      }
      if(s) 
        print s
    }	
  '
}

Displaying the prompt

Now that we have the get_git_status() function displaying the prompt is a matter of defining the PS1 variable:

alt text

Conclusion

This post was not only about setting the git repository status as a shell prompt. We also saw how to change the shell prompt dynamically, what are the possible statuses of a repository and how to get it and how to put some colors in a shell window. That was fun!

One last remark: while writing, I had to google for some information as I’m not an expert of bash or git and I came across few scripts doing the same; probably the most complete one is called git-prompt.sh. It can be found here.