Oops. You messed up with rebase and have lost a days work. Or maybe you live in fear of that happening, so you avoid using destructive operations such as rebase and reset altogether!

Reference Log

Your “Reference Log”, i.e. reflog, is a recording of every time a ref was updated in your local repository. A ref, or reference, can be thought of as a symbolic link to a git SHA (the commit hash). Examples of these are branch names, tags, HEAD, etc.

So this means every time you do something that changes your HEAD, a record is created of that in your reflog. Switch to a branch? Rebase? Reset? All of these operations will result in a new log entry! See for yourself, run git reflog in one of your projects and see what it shows you.

Garbage Collection

The second important system of git we depend on for recovery is the garbage collector. When you execute a “destructive” operation in git, AFAIK it’s never actually deleting commits locally. It’s creating new ones, and changing your refs to point at them. The old ones are still around, albeit in a “detached” state since there are no longer refs pointing to them, save for the reflog. Therefore, you need the SHA (commit hash) in order to interact with them.

git can have this property without wasting a bunch of excess space due to its use of garbage collection. When you run a git command, occasionally git will decide the repository needs to be cleaned up, and will run the garbage collector. It has many jobs, but two are of particular interest for this article:

  1. Run git reflog expire, which by default will expire all reflog entries older than 90 days, but can be configured with gc.reflogExpire
  2. Find any commits that cannot be reached via branches, tags, the reflog, etc, and delete them

Recovery

Understanding how the reflog and garbage collection interact, we know how to reach old commits that have been “destroyed” by operations like rebase or reset. This only works if we have commits to go back to though. Knowing this, I like to make a habit of committing frequently, and will often have a single commit with a WIP1 message at my HEAD, and continually amend this commit as I work with:

$ git commit --amend --no-edit

When I’m confident I have a functional unit of work2, I will amend it without the --no-edit flag, give it a nice message, and get to work on the next piece. Sometimes I end up with changes in HEAD that belong in the next commit, so I’ll:

  1. $ git reset @^ to “destroy” my WIP commit, with the contents ending up unstaged in my index
  2. $ git add -N <files> if there are any untracked files I want included
  3. $ git add -p to interactively choose which hunks to stage
  4. $ git commit

And get to work on the next piece, with my previous changes now unstaged in my index. Understanding how these systems in git work enables us to work fearlessly, and strive for better commits.

As always, please read the manpages:

$ man git-reflog
$ man git-gc

  1. Work In Progress ↩︎

  2. I believe a single commit should be a single, functional unit of work. This means it can be described by a sentence without using “and”, and is “functional” in that it could go into production on its own (tests, linting, format, etc. pass) ↩︎