News & Info

Daily Updates and Tech Chatter

Undoing Mistakes Easily in Git

When I was explaining Git stuff to a co-worker on Friday, I said, if anything ever goes wrong to just remember this to undo it:

$ git checkout @{1}

I told him that would put him back in the state he was in prior to whatever his last command was. Although then I did not go on to give a full, technical explanation of the cryptic command, as I did not think he needed to know it; I rarely need to understand the mechanics of a safety device in order to benefit from it.

But here I’m going to explain what this odd looking command is really doing. And it is worth remembering.

Whenever the tip of a branch changes—maybe you make a commit, or reset something, or simply checkout another branch—Git records that change in something called the reflog. You can run the command git-reflog in any repository to get a peek of it. It looks something like this:

$ cd ~/Scripts/Git && git reflog show ejmr/git-meta
8419d22 ejmr/git-meta@{0}: rebase -i (finish): refs/heads/ejmr/git-meta onto fadfe03
7ffaf89 ejmr/git-meta@{1}: commit: Make git-redimen work
5478546 ejmr/git-meta@{2}: commit: META tweak a comment
63b9804 ejmr/git-meta@{3}: rebase finished: refs/heads/ejmr/git-meta onto fadfe0343019350f98cbf72c883df01dd5b5d6ae
024923b ejmr/git-meta@{4}: rebase finished: refs/heads/ejmr/git-meta onto 0e728470bc140928ea6de7e8551452a860f095aa
8a53c14 ejmr/git-meta@{5}: commit: WIP Initial attempt at rewriting git-redmine to use git-meta
72f73c1 ejmr/git-meta@{6}: commit: WIP First version of git-meta
f3af9ba ejmr/git-meta@{7}: Branch: renamed refs/heads/ejmr/fix-git-redmine to refs/heads/ejmr/git-meta

This is showing me the entire reflog for my git-meta branch that I had been working on recently. Note that from its conception the branch has changed eight times, and the reflog has recorded every one faithfully. By reading it backwards you can see where I renamed an existing branch, made two commits, rebased them on other work, made two more commits, then did an interactive rebase to smash things together.

That’s all fine and great. The real benefit of this is that I can return to any of those previous states. Running the command

$ git checkout ejmr/git-meta@{6}

will put me right back at my first attempt at writing the script. This underlies something fundamentally important about Git: look at how I rebased my work. Twice. Whenever you do a rebase and look at the results in some tool like gitk you would think those old commits have gone away. Clearly this is not the case, since I can get back to them via the reflog. The point I want to stress then is this:

Git garbage collects objects with no references. The reflog counts as a reference.

You probably now have a good idea of why I told my co-worker that command. Since the reflog counts your current state as {0}, then {1} is always the last state. The only thing to explain is the absence of a branch name. If you run git-reflog without any arguments then it is the same as running

$ git reflog show HEAD

So likewise, the command I told him is the same as

$ git checkout HEAD@{1}

which moves HEAD back to its previous state. This can be a really useful way to undo some type of screw up. Accidentally mess up a rebase? Use the above. Forget the best way to undo a merge? Use the above. So on and so on.

But the reflog is not a silver bullet, for a few reasons:

  1. When you delete a branch, you also delete its reflog.
  2. Entries in any reflog are not kept longer than thirty days if they are unreachable from the current tip. Or in other words, if the reflog is the only remaining reference.
  3. Entries are not kept longer than ninety days, under any circumstances.
  4. The last two times can be configured by setting the values of gc.reflogexpireunreachable and gc.reflogexpire respectively. I do not recommend setting either to zero so that you have references to everything forever, because then Git will not garbage collect those otherwise unreachable objects, and you will be potentially wasting a lot of space.

    Anyways—remember that

    $ git checkout @{1}

    is your friend.

Tags: ,

1 Awesome Comments So Far

Don't be a stranger, join the discussion by leaving your own comment
  1. Mark Turner
    March 30, 2011 at 8:11 PM #

    I’m not sure what you’re attempting to show someone. This does not “undo” what they’ve done. This just puts you in a detached head state. The next time that branch is checked out all the stuff you thought you un-did would be there again.

    I would suggest they do something like ‘git reset HEAD@{1}’. This will reset the working tree to the previous HEAD and leave the modified file unstaged.

    Sorry for the terse response, but this post confused a colleague of mine for a while today.