Tuesday, November 27, 2012

Configure Git, Like a Boss

I have just created a Git presentation. The presentation is named Git, Practical Tips, and it contains good practices that I have picked up during my four years as a Git user.

The presentation consists of six parts, A quick introduction; History manipulation with merge, rebase and reset; Finding with Git; Configuration; Under the hood; and Interacting with Github.

If you find this interesting and would like to hear a very practical presentation about Git tips and tricks, feel free to contact me :)

In this post I will describe how to configure Git to work well from the command line. It consists of two main parts, Git configuration and Bash configuration.

I will only describe some select samples of my configuration here. If you want to see more, my configuration files are on Github.

Git Configuration

The Git configuration part is just a bunch of aliases I use. Some are simple and some are more advanced. The aliases are declared in my global git config file, ~/.gitconfig under the [alias] tag. Here are some of most important ones.

git add --patch

[alias]
ap = "add --patch"

git ap (git add --patch) is awesome. It lets me add selected parts of the changes in my working directory, allowing me to create a consistent commit with a simple clear commit message.

git add --update

au = "add --update"

git au adds all the changed files to the index. I use it mainly when I forget to remove a file with git rm and instead remove it with rm. In this case Git will see that the file is missing but not staged for removal. When I run git au it will be added to the index as if I had used git rm in the first place.

git stash save -u

ss = "stash save -u"

git ss stashes everything in my working directory, including untracked files (-u). The reason I use git stash save instead of just git stash is that it allows me to write a message for the stash, similar to a commit message.

git amend

amend = "commit --amend -c HEAD"
amendc = "commit --amend -C HEAD"

git amend lets me add more changes to the previous commit. It is very useful when I forget to add a change to the index before I commit it. It amends the new changes in the index and lets me edit the old commit message. git amendc does the same thing but reuses the old commit message.

git alias

alias = "!git config -l | grep alias | cut -c 7-"

git alias shows me all my aliases. Starting with a bang (!) is necessary to execute arbitrary bash commands. Note that the git command must be included. The code in the alias means, list configuration, find aliases, show characters 7 and on.

git log --diff-filter

fa = "log --diff-filter=A --summary"
fd = "log --diff-filter=D --summary"

git fa (find added) and git fd (find deleted) shows me a log of commits where files were added and deleted respectively. It is great for finding out how and when my files get deleted. I use it with a filename git fd my-missing-file.rb or with with grep, git fd | grep -C 3 missing.

grep -C 3 means shows me 3 lines of context around the matching line.

git log-pretty

l = "!git log-hist"
log-hist = "!git log-pretty --graph"
log-pretty = "log --pretty='format:%C(blue)%h%C(red)%d%C(yellow) %s %C(green)%an%Creset, %ar'"

git l is my main logging command it and prints a beautiful compact log. When I reuse an alias I must use the shell command alias, the bang (!), since Git does not allow me to reference an alias from another directly.

log               = The log command
--graph           = Text-based graphical representation
--pretty='format' = Format according to spec
%C(color)         = Change color
%h                = Abbreviated commit hash (6b266c2)
%d                = Ref names (HEAD, origin/master)
%s                = Subject (first line of comment)
%an               = Author name
%ar               = Author date, relative

git log --simplify-by-decoration

lt = "!git log-hist --simplify-by-decoration"

git lt (log tagged) uses --simplify-by-decoration to show a list of "important" commits. Important in this case means commits that are pointed to by a branch or tagged. It reuses the log-hist alias above.

Bash Configuration

I used to have a bunch of aliases, such as ga, gd, etc. but, now I use my Git aliases instead. But I still have configuration for command completion and a nice informative prompt.

function g()

I use the git command more than any other command during a days work. git status is the subcommand I use mostly. I have optimized for this by creating a function g() that has status as its default argument.

# `g` is a shortcut for git, it defaults to `git s` (status) if no argument is given.
function g() {
    local cmd=${1-s}
    shift
    git $cmd $@
}

The g() function gives me a lot of power out of a single character.

$ g
## master
 M README.md
?? doc.md

$ g l
* 4f71f8d (HEAD, heroku/master, master) Send 404 for missing ...
* ec00879 Added support for options Anders Janmyr, 5 weeks ago
* 09c178f (origin/master) id cannot be a number Anders Janmyr, 6 weeks ago
* e561d03 Send status and send in one call Anders Janmyr, 6 weeks ago
* 9615be5 Added some more logging Anders Janmyr, 6 weeks ago
* de4730e Improved the code somewhat Anders Janmyr, 6 weeks ago
* 1f3f763 Added allow methods header Anders Janmyr, 6 weeks ago
* ca3065c Added filter to documentation Anders Janmyr, 6 we

function gg()

My second (and last) function is gg().

# Commit pending changes and quote all arguments as message
function gg() {
    git ci -m "$*"
}

gg() allows me to type a commit message without any quotes.

$ gg Added todo list to the Readme
[master 98556af] Added todo list to the Readme
 1 file changed, 1 insertion(+)

bash-completion

Installing bash-completion gives me command completion for commands, subcommands and more.

# An example
$ git rem<TAB> sh<TAB> o<TAB>
# will complete to
$ git remote show origin

I use Homebrew to install Git, brew install git. It gives me a new version of Git. It also installs git-completion.bash in /usr/local/etc/bash_completion.d/.

I use the same configuration on Ubuntu and I check for the file in /etc/bash-completion.d/ too.

# Prefer /usr/local/etc but fallback to /etc
if [ -f /usr/local/etc/bash_completion.d/git-completion.bash ]
then
    source /usr/local/etc/bash_completion.d/git-completion.bash
elif [ -f /etc/bash_completion.d/git ]; then
    source /etc/bash_completion.d/git
fi

This is great but, what about my beautiful little g() function? How do I make it work with command completion? It turns out to be quite easy. Include the following little snippet in a configuration file, such as .bashrc.

# Set up git command completion for g
__git_complete g __git_main

The snippet reuses the functions, __git_complete and __git_main included with git-completion.bash to make completion work with g too. Lovely!

bash-prompt

In later versions of Git, the prompt functionality has been extracted out into its own script, git-prompt.sh. I include it like this.

if [ -f /usr/local/etc/bash_completion.d/git-prompt.sh ]
then
    source /usr/local/etc/bash_completion.d/git-prompt.sh
fi

I configure my prompt like this, it contains a little more magic than the plain Git configuration. I put it in one of my bash configuration files, such as .bashrc.

function prompt {
  # Check exit status of last command
  if [[ "$?" -eq "0" ]]; then
    # If it is OK (0) color the prompt ($) green
    local status=""
    local sign=$(echo -ne "\[${GREEN}\]\$\[${NO_COLOR}\]")
  else
    # If not OK (not 0) color the prompt ($) red and set status to exit code
    local status=" \[${RED}\]$?\[${NO_COLOR}\] "
    local sign=$(echo -ne "\[${RED}\]\$\[${NO_COLOR}\]")
  fi
  # Get the current SHA of the repository
  local sha=$(git rev-parse --short HEAD 2>/dev/null)
  # Set the prompt
  # \!                 - history number
  # :                  - literal :
  # \W                 - Basename of current working directory
  # $sha               - The SHA calculated above
  # $(__git_ps1 '@%s') - literal @ followed by Git branch, etc.
  # $status            - The exit status calculated above
  # $sign              - The red or green prompt, calculated above
  export PS1="[\!:${LIGHT_GRAY}\W${NO_COLOR} $sha${GREEN}$(__git_ps1 '@%s')${NO_COLOR}$status]\n$sign "
}

# Tell bash to invoke the above function when printing the prompt
PROMPT_COMMAND='prompt'

The function __git_ps1() can further be configured with some environment variables. This is what use.

# Git prompt config
export GIT_PS1_SHOWDIRTYSTATE=true
export GIT_PS1_SHOWUNTRACKEDFILES=true
export GIT_PS1_SHOWUPSTREAM="auto"
# export GIT_PS1_SHOWSTASHSTATE=true

The resulting prompt looks like this:

The different signs to the right indicate:

# * - Changed files in working dir
# + - Changed files in index
# % - Untracked files in working dir
# < - The branch is behind upstream
# > - The branch is ahead of upstream (Yes, it can be both)

More info can be found in git-prompt.sh.

Credits

Obviously I have not figured this out all by myself. Here are some of my sources:

2 comments:

Jonathan Jackson said...

In ZSH your g() command has to check the value of $#. Working example in gist: https://gist.github.com/4953414. Great post.

Anders Janmyr said...

@Jonathan, I'm glad you liked it. I used to have a little ZSH envy until I tried to switch and realized how used to Bash I am.