The Nuclear Squid Musings on stuff and other things.

A few of my Git tricks, tips and workflows

Note: A german translation is available here

This post is based on a talk I gave at the 18th Cocoaheads Meetup Vienna (CHW018) on Feb 17th, 2011. It is an annotated tour of my Git config, Git related scripts and commands, and various other tips and tricks I picked up over the years. You can find most of these things in my dotfiles repo, as well with a lot of other stuff, like parts of my Zsh config. Patches welcome.

Warning: Some of these tricks and tips are specific to my setup (Mac OS X Snow Leopard, Git 1.7.4) and workflow(s), and might not quite work for you as described. I also assume you have basic knowledge of the command line.

The Basics

Getting Help

No matter what you’re doing with Git, there’s always some kind of documentation that can help you out. But you should know where to get what. There are basically two ways on the command line.

The first one is typing git <command> -h (replacing <command> with the command in question of course), and Git will print a short overview of the call syntax as well as the most important options for you.

The second one is having a look at the manpages themselves. The fastest way to get to them is by typing git help <command>, and git will open them for you. They are more thorough, but aren’t for the faint of heart either.

On the web, there are a few more options. The Git Homepage lists quite a few introductions, tutorials, guides as well as online books that can help you out.

Git Autocompletion for your shell

I use Git primarily from the command line. Luckily, enabling the Git completion for both Bash and Zsh is quite easy if you used Homebrew to install Git. Simply add the following to your ~/.bashrc:

source `brew --prefix git`/etc/bash_completion.d/git-completion.bash

If you got Git some other way, check if that includes the completion file. If not, you can always clone the git repo, and copy/link it from there.

If you use Zsh, like me, there’s a bit more involved. Add the following to your ~/.zshrc (adjust as necessary for your installation method):

# Enable bash completion for git
# This allows git-completion to work properly
autoload bashcompinit
bashcompinit

source `brew prefix`/etc/git-completion.d

Update 2011-04-05: If you’re using a newer ZSH (I’ve tested this with 4.3.9), then you can skip all of the above, as Git completion will automagically work for you (if you already have autoload -U compinit && compinit in your .zshrc).

If your version of ZSH is older, you can download the git completion from the ZSH repository (h/t graywh on Hacker News)

Alias git to ‘g’

I also alias git to g, saving me 66% on typing time! Also, I’m lazy. Add the following to your .bashrc or .zshrc:

alias g='git'

Now, you’ll probably also want to have the Git Autocompletion when you’re using g as well. Add this to your .bashrc or .zshrc:

# Autocomplete for 'g' as well
complete -o default -o nospace -F _git g

git-config(1)

In here, you can store all the global Git configuration settings. The .gitconfig file uses the INI file format to store settings, but you can also set (and get) all values by using git config.
The most basic settings you should do are your name and eMail, both of which are included in every commit you make:

git config --global user.name "Markus Prinz"
git config --global user.email "markus.prinz@nuclearsquid.com"

Not that I’m telling you how to run your life or anything, but you probably want use your own name and eMail address instead of mine.

Over time, you will accumulate your own set of configs that tweak Git to your liking. Here are a few of my settings that I find very useful:

  • Allow all Git commands to use colored output, if possible

    git config --global color.ui auto
    
  • Disable the advice shown by Git when you attempt to push something that’s not fast forward-able

    git config --global advice.pushNonFastForward false
    
  • Disable “how to stage/unstage/add” hints given by git status:

    git config --global advice.statusHints false
    
  • Tell Git which whitespace problems it should recognize, namely any whitespace at the end of a line, as well as mixed spaces and tabs:

    git config --global core.whitespace trailing-space,space-before-tab
    

    See the man page for more possible options on this.

  • Allow git diff to do basic rename and copy detection:

    git config --global diff.renames copies
    
  • Tell git diff to use mnemonic prefixes (index, work tree, commit, object) instead of the standard a and b notation:

    git config --global diff.mnemonicprefix true
    
  • When branching off a remote branch, automatically let the local branch track the remote branch:

    git config --global branch.autosetupmerge true
    
  • When pushing without giving a refspec, push the current branch to its upstream branch. See the git config man page for more possible options.

    git config --global push.default tracking
    
  • Enable the recording of resolved conflicts, so that identical hunks can be resolved automatically later on.

    git config --global rerere.enabled true
    

    You may also want to investigate the rerere.autoupdate setting.

  • Always show a diffstat at the end of a merge:

    git config --global merge.stat true
    

Now, you’ll notice that for each and every git config I used the --global option. The reason for that is that Git not only looks at your global gitconfig (located at ~/.gitconfig), but also a repository-specific config (.git/config). So you can customize all these settings for each of your repository to your liking, just run the git config command in your repository without the --global flag.

gitignore(5)

You don’t want to commit all your files into your repository. Things like temporary files, logs, configurations that are specific to a computer, files that are for testing only, private keys for code signing or files that can be easily regenerated all don’t belong in your repository. However, they will still show up whenever you type git status, even though they are purely noise at that point.

For this reason you can specify what files you want Git to ignore in a special file called .gitignore. Placed at the root of the repository, it contains glob patterns specifying all the files you want Git to ignore. Any file (and directory) matched by these globs won’t show up in git status, and if you try to add them via git add, Git will refuse to do so (you can still add them by specifying the -f flag when adding).

For example, Rails 3 projects use the following .gitignore by default:

.bundle
db/*.sqlite3
log/*.log
tmp/

This tells Git to ignore the .bundle directory, all files ending in .sqlite3 in the db directory, all the logs in log, and everything in the tmp directory.

Depending on what kind of project you’re working on, you’ll want to specify your own patterns. To save the work, GitHub has a gitignore repository with glob patterns for many programming languages, frameworks and platforms.

There are also Xcode 3 & 4 specific .gitignore templates.

However, you can also tell Git to use a global .gitignore file, so you can exclude common files or ones that are specific to your platform. I keep mine at ~/.gitignore, and it contains excludes like .DS_Store and Icon?. Now all you need to do is tell git where it can find that file:

git config --global core.excludesfile '~/.gitignore'

Now you’ll never have to worry about any of these files showing up in your git status output any more.

gitattributes(5)

This file allows you to specify attributes for given paths. It’s not a feature you’ll need often, but it can be incredibly handy.

For example, in Rails projects, the db/schema.rb file can often lead to merge conflicts that are easily resolved by figuring out which file is the most recent once, and then use that entire file as the resolution. But instead of having to do that by hand every time, we can tell Git to do it for us.

First, you’ll need to add this to your ~/.gitconfig:

[merge "railsschema"]
        name = newer Rails schema version
        driver = "ruby -e '\n\
                system %(git), %(merge-file), %(--marker-size=%L), %(%A), %(%O), %(%B)\n\
                b = File.read(%(%A))\n\
                b.sub!(/^<+ .*\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n=+\\nActiveRecord::Schema\\.define.:version => (\\d+). do\\n>+ .*/) do\n\
                  %(ActiveRecord::Schema.define(:version => #{[$1, $2].max}) do)\n\
                end\n\
                File.open(%(%A), %(w)) {|f| f.write(b)}\n\
                exit 1 if b.include?(%(<)*%L)'"

This specifies a new merge strategy called railsschema, along with the necessary command that Git needs to execute to resolve conflicts. The command itself is just a piece of Ruby code that figures out which schema.rb is the most recent one.

Then, in your ~/.gitattributes file, add the following line:

db/schema.rb merge=railsschema

This tells Git than whenever it is merging db/schema.rb, it should use our custom merge strategy instead of the default one. And we won’t ever have to manually merge schema.rb again.

You can also use this file to specify which end-of-line conversions Git should perform for certain file types, or files that it should treat as binary files. See the man page for more details.

githooks(5)

Every time Git does something, you usually have the option of taking an action before or after Git has done its bit. An example of this would be a pre-commit hook, that you could use to prevent a commit from happening according to some custom criteria.

All hooks are simple executable files, and can be written in whatever way you want to. I usually use basic shell script for simple stuff, and Ruby for the more complex hooks.

Each Git repository contains a number of sample hooks under .git/hooks/. To activate a hook, create or link the script/program to the hook name you wish to use in .git/hooks/, and make sure it is executable.

Hooks may get certain parameters that can help them determine what has happened. For example, the post-checkout hook gets the ref of the previous head, the ref of the new head, and a flag indicating wether the checkout was due to changing branches.

The githooks(5) man page lists all available hooks, but the ones you’ll probably be interested in most of the time are these:

  • pre-commit: This hook is executed before git commit creates a new commit. If this hook returns a non-zero status code, the commit is aborted. Very useful to prevent people from checking in stuff they shouldn’t (like passwords), or performing things like syntax checks to ensure that only valid code is being checked in.
  • post-commit: Executed after a new commit has been created. Useful for things like notifications.
  • post-checkout: Run after any kind of checkout operation, like changing branches. I use this hook in Ruby projects to determine if the Gemfile has
    changed, and run bundle install if that is the case.

Using an external diff viewer (Kaleidoscope)

The built-in diff viewer is great most of the time, but sometimes you want to diff non-text files, or simply need a better visualization of what’s going on.

For this reason, Git allows you to specify external diff viewers. I use the excellent Kaleidoscope (OS X only, sorry), which can not only diff text files, but also images. Other options include FileMerge.app, which is included with Apple’s Developer Tools.

Setting up an external diff viewer usually involves installing some kind of command line app or script. In Kaleidoscope’s case, all you need to do is follow the instructions under Kaleidoskope->Integration to install the command line tool, and add the relevant lines to your .gitconfig.

Now all I need to do is call git difftool instead of git diff (difftool takes all the same options as diff), and Git will invoke Kaleidoscope for me.

Git template

When Git creates a new repository, it uses a template to populate certain files and directories. Assuming your Git is installed in /usr/local, you’ll find this template under /usr/local/share/git-core/templates. This is mostly useful to install your custom default hooks, so you don’t have to remember adding them when setting up a new repository.

If you’ve changed the template after creating a repository, you can simply run git init inside that repository again, and it will pick up the new template.

External Apps

There are a few GUI applications that make your life with Git a bit easier, especially when it comes to stuff like history visualization. Here are a few of them.

GitX

GitX is my favorite GUI for examining a repository’s history. It shows you all the diffs in a commit, you can search commits, and it even includes some nice functions to create new commits from within GitX, making it a good tool for people who prefer GUIs over the command line. GitX is open source and available on GitHub.

However, the official version is a bit outdated, so get Brotherbard’s improved GitX fork instead, which offers a few more features and improvements over the original.

Gitbox

Gitbox (Also available in the Mac App Store, affiliate link, proceeds go towards Cocoaheads Austria) is another Mac GUI and offers more features than GitX. However, I haven’t used it much, and thus can’t really say much about it. Free for up to 3 repositories, and $39 for full version, so it won’t hurt you to try it out.

Tower

Tower is a much more powerful than GitX and Gitbox, but is also more complex. If you really don’t like the command line, this might be the client for you. €49, 30 day trial available.

iOS Apps

There are also a few apps available for the iPhone and the iPad, but with the exception of Cardff git, they are all geared towards viewing your GitHub account and repositories.

(Note: The App Store links are affiliate links, proceeds will go towards Cocoaheads Austria)

  • iOctocat (€3.99, for iPhone, Source)

    A full-blown GitHub companion for your iPhone. Allows you to view feeds, repositories, commits, and user profiles. Can also examine, open, edit or close issues.

  • Cardff Git 1.7 (€0.79, for iPhone)

    A small cheat sheet app that acts as a handy reference for most Git commands.

  • GitHub Viewer (€0.79, for iPad)

    Allows you to view your GitHub repositories, histories, as well as users on GitHub.

  • GitHub Viewer Light (for iPad)

    Same as GitHub Viewer, but without the ability to view private repositories.

  • githood (€0.79, for iPhone)

    Allows you to track your GitHub repositories, including diffs and feeds.

Stacked Git (stGit)

Unlike the other programs, Stacked Git is not a GUI application. It is a command line app that enhances Git by allowing you to manage a stack of patches on top of your repository. All these patches are stored as ordinary Git commits, but StGit helps you managing these patches.

I used to use this program quite a lot, but it has since been superseded with git rebase and git stash.

Revisions

Most Git commands expect some kind of revision. Usually you pass in a branch name or a SHA1 of a specific commit, but Git revisions are much more powerful than that.

First, there is a special revision called head (also known as HEAD). head is whatever point your working directory currently is at, usually the tip of a branch.

Second, you can easily reach older commits given a starting ref without having to know their SHA1. Let’s say you want to specify the commit that came before the current one (also called “parent”): Simply type head^. The great-grandfather? head^^^. However, the farther back you go in history, the more cumbersome it gets. So Git also offers an alternate syntax for this: The great-grandfather can also be specified using head~3.

Want to reach the commit whose commit message matches a certain string? Use :/really awesome commit to find that really awesome commit.

branch@{yesterday} will give you the revision that branch was at yesterday, branch@{2 days 3 hours 4 seconds ago} is the branch, well, 2 days, 3 hours and 4 seconds ago.

I’ve only scratched the surface of what’s possible, so make sure you read the man page.

Commands

Here are a few commands that you may not know yet, or that offer options you haven’t been aware of so far.

git-stash(1)

git stash is something that can be incredibly handy. What it does is very simple: If you run git stash save, it will take all the changes you have in your working directory and the index, and save them away, leaving you with a clean working directory. Once you’ve done what you wanted to do, you can restore your changes by running git stash pop.

But it gets better: since git stash saves your changes in a commit, you can stash more than once, and you’ll get a nice list of commits with your stashed changes. git stash will also keep track of what branch you stashed your changes on, so you’ll know later on what stashed change exactly you want to restore.

git stash list will list all the stashes you’ve saved away, git stash apply will apply the topmost stashed commit onto your working directory, git stash pop will also remove the stashed commit in addition to applying it. git stash clear throws away all your stashes, and git stash drop allows you to remove a single stash.

git-rebase(1)

git rebase can be a very tricky command, although its purpose is very simple: Given a range of commits as well as a starting point, it will port the given commits onto that starting point, preserving the commits themselves, but also allowing you to keep a linear history (that is, no merge commit).

Most of the time you’ll give it just one ref: The branch you’ll want your current branch to rebase onto. git rebase then figures out where those two branches diverged, and use that as the beginning of the commit range. If you pass it a second argument, git rebase will first switch to that branch before porting your commits.

It also has one very useful flag: --interactive. If you pass this flag, Git will launch your favorite editor with a list of all the commits it is going to rebase. All commits are prefixed with pick by default, but you can either drop commits entirely (just delete their line), reorder them to your liking, or edit if you’d like to edit or amend the commit (note that this also allows you to split commits), reword if you’d just like to change the commit message, and squash or fixup if you want to squash the commit with the previous one. fixup will reuse the previous commit’s message, while squash allows you to edit the new commit’s message.

The interactive mode is very useful when you’re working on some idea you’ve had, make a commit, and later on realize that it isn’t quite working out that way. You commit a fix, but the commit it fixes is already buried in other commits, so you can’t simply amend. git rebase --interactive allows you squash those two commits together and make it look like you never made the mistake in the first place.

git-push(1)

This is a command that you’re probably using often already, but it gained a few more useful options in recent versions:

  • -u or --set-upstream: When doing git push <repo> local-branch:remote-branch, you can use -u to tell Git to set up remote-branch as the default upstream branch, so you’ll only need to do git push in future.
  • --delete: Instead of pushing the specified refs, it will instead delete them from the remote repository.

git-reflog(1)

Whenever an operation changes the tip of a branch, the Reflog mechanism records the old and new locations. git reflog allows you to access and manage this information.

This is very useful and potentially life saving after any command that (seemingly) changes the history of your repository, like git rebase or git filter-branch. While these commands do indeed change commits, the old commits aren’t deleted, they are merely unreachable by normal means (This is also the reason why Git will occasionally run git gc on your repo). However, using the Reflog mechanism you can find out the SHA1’s of these commits, and restore them if you need to.

git reflog lists all the recorded reflog information, and you can then specify any point in the reflog using the syntax head@{0} (where head is any kind of ref, and 0 to specify the very first reflog entry). For example, if you just did a rebase, but decided that you messed up somehow, you can restore your branch to its state before the rebase by using this command:

git reset head@{1}

Getting diffstats

git diff --stat
git log --stat

Not much to add, really.

Scripts

These are a few of my custom scripts that I use in my Git workflows. They have usually been copied from somewhere else, and then modified for my needs. Find their current versions in my dotfiles repo.

One trick that not too many people are probably aware of: If you have a script named git-foo in your $PATH, and you call git foo, Git will actually automatically call you script for you. This way you can easily extend Git with seemingly built-in commands, and you don’t have to worry about any aliases like g.

git-up, git-reup

git pull will only tell you that there were new commits, and display a diffstat to show you which files changed. git up does a bit more: It will not only pull new commits, but also show you a shortlog of all the new commits, giving you a quick overview of all the new commits.

However, when invoked as git reup, it essentially does the same thing, with one difference: Instead of performing a git pull, it will do a git pull --rebase (as well as a git stash save beforehand and git stash pop afterwards if you have a dirty working directory), thus preserving a linear history. And in addition to the log, you’ll also get a diffstat to see all the changes.

git-new-workdir

Sometimes you want two branches of your repository checked out at the same. One solution to this is to simply create a second, local clone of your repo, and check out the other branch there. But now you have to keep that second repository in sync with the original one, since commits to the one won’t automagically migrate to the other.

This is where git new-workdir comes in: You pass it the path to the original repository, along with the path where you want the second repository to reside, optionally telling it which branch you want to use for your second repo. git new-workdir will then set up a second repository in such a way that it is effectively a second checkout, and you won’t have to push or pull to keep them in sync.

git-wtf

This is a very handy script: Run it on any branch that has a remote tracking branch, and it will tell you wether they are in sync, or which commits (on each branch) are missing from the other, so you can quickly figure out the state of your local and remote branch.

Example output:

Local branch: master
[ ] NOT in sync with remote (you should push)
    - Add vim-endwise [af43b93]
    - Update vim plugins [a926c01]
    - Add TComment plugin for Vim [035d3b9]
    - Add matchit plugin for Vim [7b76c04]
    - Add vim-markdown plugin [a6e7c19]
    ... and 3 more (use -A to see all).
Remote branch: origin/master (git@github.com:cypher/dotfiles.git)
[x] in sync with local

NOTE: working directory contains modified files.

Aliases

Here are some of the aliases that I use that aren’t simply abbreviations of my common git operations.

git l

Shows a one-line history of the current branch, along with any ref names of commits.

git lg

Similar to git l, but uses the graph format to visualize branches & merges.

git ls-ignored

Lists all files that Git ignores right now due to .gitignore.

git amend

Quickly amend the previous commit with the currently staged changes, without editing the commit message.

git ca

Same as git amend, but allows you to edit the commit message.

git wd, git wds

Shows a diff that not only highlights the changed lines, but also the actual words and characters in each line that have changed. git wds does the same for staged changes.

GitHub

GitHub specific tools and workflows I use.

github gem

Install via RubyGems as follows:

gem install github
# Use sudo if necessary

github is a command line tool that helps you interact with GitHub without leaving the command line, like creating a new repository, fetching forks of a repo, forking, examining the network of a repository, generating pull requests, and so on.

It installs itself as both github and gh, and passing -h or --help will give you all the available commands.

Note: If you’re using Ruby 1.9, the installation process may fail with an error message like text-hyphen requires Ruby version < 1.9. If you pass the --force flag to gem, it will install it anyway, and so far I haven’t had any problems.

git-pulls

git pulls is a new tool that helps you managing and merging pull requests.

Install it via RubyGems:

gem install git-pulls
# Use sudo if necessary

It is then available via git pulls <subcommand>. If you leave out the subcommand, you’ll get a short overview of all available commands. list will list all open pull requests, show will show you a specific pull request. merge will make you a cup of coffee (actually, it’ll merge the given pull request, but you probably guessed that already, didn’t you?)

It’s still under active development, so make sure to always run the latest version (gem update git-pulls).

Pull Requests

If someone forks your repo on GitHub, and pushes their commits to their fork, they will probably want you to eventually merge their changes back.

GitHub supports this workflow using a mechanism called Pull Requests. Whenever you want someone else to merge one or more commits into their repository, you can issue a Pull Request. In addition to the commits, the pull request also contains a description of all the commits contained in the request. After you’ve created one, other people can then comment on the request itself, view all the changes it introduces, and even comment on specific lines. The issuer can also amend his pull request with additional commits to address issues others have found. This makes pull requests an excellent code review tool.