git aliae so that you never lose work (part 2)

Conrad Irwin — May 2012

Git is an incredibly powerful tool for version control. Much of this power stems from its underlying append-only object database which ensures that once you’ve made a commit it can never be changed or unmade. This is great because it means you can refactor history safe in the knowledge that you can git reset back to a working version if something goes wrong.

One thing I’ve yearned for however is the same functionality for uncommitted changes. Wouldn’t it be great if every time git modified your working tree, it took a backup?

Dangerous git commands

Luckily the number of git commands that actually modify the working tree is small, in fact there are only three commands that you’ll usually come across:

  1. git checkout (with -f, or with a filename)
  2. git reset --hard
  3. git clean -f

If you’ve used git for a significant length of time, and are not some kind of super-hero, then I can guarantee that you’ll have lost work with each of these.

It was after one particularly badly timed git checkout -f that I finally decided that something needed to be done. While retyping out the hours of code that I’d accidentally deleted I came up with a plan: Every time I wanted to do a git checkout -f I’d first store a copy of my working tree into the object database. Then whenever I realise that I’ve thrown away the wrong changes, I can recover them.

git-cof && git-foc

To this end I wrote git cof. All it does is take a backup of the working tree, and then forward any arguments on to `git checkout -f`. To facilitate undoing this, I also wrote git foc (the name is intentionally evocative of the expletive I no longer need to use).

git cof can be used as a drop-in replacement for all uses of git checkout, (excepting -m and -p because those flags are incompatible with -f). This means that if you use it when changing branch, or when checking out a particular file, you’ll also get a backup of your unsaved changes.

git foc just automates the restore process. It finds the latest backup commit made by git cof, and re-instates any changes that git cof removed. You do have to be somewhat careful with git foc as if you make conflicting changes between running git cof and git foc, you’ll end up with a merge conflict.

Although git foc only allows you to restore the most recent backup, all the other backups are also preserved for at least two weeks. If you need them back you can tail .git/tree_backups and use normal git commands to reinstate changes from those commits.

git reset –keep

While I was considering writing an equivalent script to wrap git reset --hard I discovered git reset --keep. This mode of git reset was added in git version 1.7.1, and is exactly like git reset --hard except that it preserves any uncommitted changes in the working tree.

Concretely that means that it makes your current branch point to the commit you specify, and then it re-applies all of your uncommitted changes. If it can’t do this properly, then it will abort and tell you what went wrong.

This means that there’s now no reason to use git reset --hard ever. You can always use git reset --keep. In the rare cases that it aborts because you have uncommitted changes that conflict with the reset, you can use git cof to throw away your local changes in a safe way, and then retry.

Preserving untracked files

One thing that git cof doesn’t do is preserve files that are untracked (i.e. you haven’t yet git added them). This is for consistency with most git commands which refuse to touch untracked files; and also to ensure that it doesn’t accidentally add a large number of useless files to your git object database.

This means that git cof doesn’t help avoid the data-loss caused by git clean -f yet. I’ve not implemented a solution that does and instead just avoid using git clean at all. Instead I use rm manually as I find that copying and pasting the list of files to remove one-by-one makes me think a little harder about whether I actually want to delete it.

Summary

Internalising three things can thus help you avoid losing most work with git:

  1. Use git cof instead of git checkout
  2. Use git reset --keep instead of git reset --hard
  3. Use rm instead of git clean -f

I’ve found that these rules have saved me a considerable amount of time that would otherwise be wasted re-writing lost code.

To install git aliae:

git clone https://github.com/ConradIrwin/git-aliae
# Add the following line to your ~/.bashrc
export PATH=$PATH:/path/to/git-aliae/bin

Please report bugs and feature requests to GitHub issues.