Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Reflogs

Gtotw 2011 Tip Git

This week's Git Tip of the Week is about recovering work with the reflog. You can subscribe to the feed if you want to receive new instalments automatically.


Reflogs

The Git reflog is very different from the Hg reflog of the same name. Hg reflogs are equivalent to CVS's ,v files. (Actually, they're RCS's, but never mind that now ...)

A Git reflog is a list of hashes, which represent where you have been during commits. Each time a branch is updated to point to a new reference, an entry is written in the reflog to say where you were. Since the branch is updated whenever you commit, the git reflog has a nice effect of storing your local developer's history.

Furthermore, the pointer in the reflog points to a commit object, which in turn points to a tree object, which represents a directory-like structure of folders and files. So whilst the reflog is active, you can go back and see what changes you have made – and even recover specific files from previous commit versions. With Git, you never really lose anything; even if you've done a filter-branch to re-write history, you're only a reflog entry away from getting it all back.

To see what the reflog is all about, run git reflog from an active Git repository. It might look something like this:


9bdbd83 HEAD@{0}: commit: Adding  build script
86a7a39 HEAD@{1}: commit (amend): Updating commit message
325e0af HEAD@{2}: commit (amend): Example Project (fix typo)
06bf85e HEAD@{3}: commit (initial): Example project

The first number is simply the commit hash at the point the change was made. Even though these don't represent linear history (you'll see a couple of (amend) listed there), these are the sequence of actions taken on the local repository, in the order they were done.

The second is the state of HEAD, along with the number of changes. In this case, we have HEAD@{0}, which means where HEAD is now; HEAD@{1} is where HEAD was previously, and so on.

The final part is the type; whether it is a commit or an amended commit, and the commit subject. This is often helpful to remember where the code was, especially if it isn't part of the linear history. It also contains other operations, such as checkout, merge and reset:


abec02f HEAD@{0}: merge foo: Merge made by recursive.
9bdbd83 HEAD@{1}: 9bdbd83: updating HEAD
2d90ece HEAD@{2}: merge foo: Fast-forward
9bdbd83 HEAD@{3}: checkout: moving from foo to master
2d90ece HEAD@{4}: commit: hello
9bdbd83 HEAD@{5}: checkout: moving from master to foo

Reflog references

Most git commands accept a number of different references to point to a commit. For example, you can run git checkout master, git checkout abec02f and git checkout mytag. However, you can also checkout references by reflog as well.

In the example above, we can run git checkout 290ece, or we can refer to it as git checkout HEAD@{2}. Provided we haven't committed anything else (which would change the reflog), these two variations have the same effect.

You can use this to implement a crude form of undo:


git config --global alias.undo  "reset HEAD@{1}"

This will cause you to revert to the previous action (whether it was a commit or otherwise).

Reflog references revisited

Although we've been using HEAD here, reflogs are more general than just HEAD. The general representation is name@{qualifier}.

In fact, stashes (covered last week) are a specific form of reflog, whose name is stash. Not only that, but other branches can be referred to by their reflog as well.

All of the branch reflogs are stored under .git/logs/refs/heads/. (There's also one under .git/logs/HEAD, as well as .git/logs/refs/stash if you've used a stash before.)

We can identify reflogs for a specific branch as well:


$ git reflog show master
abec02f master@{0}: merge foo: Merge made by recursive.
9bdbd83 master@{1}: 9bdbd83: updating HEAD
2d90ece master@{2}: merge foo: Fast-forward
9bdbd83 master@{3}: commit: hello
$ git reflog show foo
2d90ece foo@{0}: commit: hello
9bdbd83 foo@{1}: branch: Created from HEAD

Since all of these are valid git references, we can perform diffs against them, e.g. git diff foo@{0} foo@{1}.

Timed reflogs

Since each reflog has an implicit time associated with it, you can filter not only by history, but also on time as well. Various supported forms include:

  • 1.minute.ago
  • 1.hour.ago
  • 1.day.ago
  • yesterday
  • 1.week.ago
  • 1.month.ago
  • 1.year.ago
  • 2011-05-17.09:00:00

The plural forms are also accepted (e.g. 2.weeks.ago) as well as combinations (e.g. 1.day.2.hours.ago).

The time format is most useful if you want to get back to a branch's state as of an hour ago, or want to see what the differences were in the last hour (e.g. git diff @{1.hour.ago}). Note that if a branch is missing, then it assumes the current branch (so @{1.hour.ago} refers to master@{1.hour.ago} if on the branch master.

Summary

Git never really loses anything, even if you perform filter branching or commit amending. The commit, tree and contents are still stored in the repository and the reflog still maintains pointers to those previous commits.

Each time you update a branch, it stores the set of past revisions in a reflog, which is both stored against HEAD and against a particular branch (including a special reflog for stashes).

The reflogs stay until expired (which can be done with the git reflog expire command). The default for unreachable commits is 30 days (or the gc.reflogExpireUnreachable config value) or, for reachable commits, 90 days (or the gc.reflogExpire config value).


Come back next week for another instalment in the Git Tip of the Week series.