A Guide To Undoing Mistakes With Git (Part 2)

About The Author

Tobias Günther is a co-founder of Tower, the popular Git desktop client that helps more than 100,000 developers around the world to be more productive with Git. More about Tobias ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 190.000 folks.

Quick summary ↬ Mistakes. These cruel villains do not even stop at the beautiful world of software development. But although we cannot avoid making mistakes, we can learn to undo them! This article will show the right tools for your daily work with Git. You might want to check the first article of the series as well.

In this second part of our series on “Undoing Mistakes with Git”, we’ll bravely look danger in the eye again: I’ve prepared four new doomsday scenarios — including, of course, some clever ways to save our necks! But before we dive in: take a look at the check out previous articles on Git for even more self-rescue methods that help you undo your mistakes with Git!

Let’s go!

Recovering a Deleted Branch Using the Reflog

Have you ever deleted a branch and, shortly after, realized that you shouldn’t have? In the unlikely event that you don’t know this feeling, I can tell you that it’s not a good one. A mixture of sadness and anger creeps up on you, while you think of all the hard work that went into that branch’s commits, all the valuable code that you’ve now lost.

Luckily, there’s a way to bring that branch back from the dead — with the help of a Git tool named “Reflog”. We had used this tool in the first part of our series, but here’s a little refresher: the Reflog is like a journal where Git notes every movement of the HEAD pointer in your local repository. In other, less nerdy words: any time you checkout, commit, merge, rebase, cherry-pick, and so on, a journal entry is created. This makes the Reflog a perfect safety net when things go wrong!

Let’s take a look at a concrete example:

$ git branch
* feature/login
master

We can see that we currently have our branch feature/login checked out. Let’s say that this is the branch we’re going to delete (inadvertently). Before we can do that, however, we need to switch to a different branch because we cannot delete our current HEAD branch!

$ git checkout master
$ git branch -d feature/login

Our valuable feature branch is now gone — and I’ll give you a minute to (a) understand the gravity of our mistake and (b) to mourn a little. After you’ve wiped away the tears, we need to find a way to bring back this branch! Let’s open the Reflog (simply by typing git reflog) and see what it has in store for us:

Git’s Reflog protocols all major actions in our local repository
Git’s Reflog protocols all major actions in our local repository. (Large preview)

Here are some comments to help you make sense of the output:

  • First of all, you need to know that the Reflog sorts its entries chronologically: the newest items are at the top of the list.
  • The topmost (and therefore newest) item is the git checkout command that we performed before deleting the branch. It’s logged here in the Reflog because it’s one of these “HEAD pointer movements” that the Reflog so dutifully records.
  • To undo our grave mistake, we can simply return to the state before that — which is also cleanly and clearly recorded in the Reflog!

So let’s try this, by creating a new branch (with the name of our “lost” branch) that starts at this “before” state SHA-1 hash:

$ git branch feature/login 776f8ca

And voila! You’ll be delighted to see that we’ve now restored our seemingly lost branch! 🎉

If you’re using a Git desktop GUI like “Tower”, you can take a nice shortcut: simply hit CMD + Z on your keyboard to undo the last command — even if you’ve just violently deleted a branch!

A desktop GUI like Tower can make the process of undoing mistakes easier.
More after jump! Continue reading below ↓

Moving a Commit to a Different Branch

In many teams, there’s an agreement to not commit on long-running branches like main or develop: branches like these should only receive new commits through integrations (e.g. merges or rebases). And yet, of course, mistakes are inevitable: we sometimes forget and commit on these branches nonetheless! So how can we clean up the mess we made?

Moving a commit to its correct destination branch
Our commit landed on the wrong branch. How can we move it to its correct destination branch? (Large preview)

Luckily, these types of problems can be easily corrected. Let’s roll up our sleeves and get to work.

The first step is to switch to the correct destination branch and then move the commit overusing the cherry-pick command:

$ git checkout feature/login
$ git cherry-pick 776f8caf

You will now have the commit on the desired branch, where it should have been in the first place. Awesome!

But there’s still one thing left to do: we need to clean up the branch where it accidentally landed at first! The cherry-pick command, so to speak, created a copy of the commit — but the original is still present on our long-running branch:

A copy of the commit on the correct branch, but the original is still shown to be on the wrong branch
We’ve successfully created a copy of the commit on the correct branch, but the original is still here — on the wrong branch. (Large preview)

This means we have to switch back to our long-running branch and use git reset to remove it:

$ git checkout main
$ git reset --hard HEAD~1

As you can see, we’re using the git reset command here to erase the faulty commit. The HEAD~1 parameter tells Git to “go back 1 revision behind HEAD”, effectively erasing the topmost (and in our case: unwanted) commit from the history of that branch.

And voila: the commit is now where it should have been in the first place and our long-running branch is clean — as if our mistake had never happened!

Editing the Message of an Old Commit

It’s all too easy to smuggle a typo into a commit message — and only discover it much later. In such a case, the good old --amend option of git commit cannot be used to fix this problem, because it only works for the very last commit. To correct any commit that is older than that, we have to resort to a Git tool called “Interactive Rebase”.

A commit message worth changing
Here’s a commit message worth changing. (Large preview)

First, we have to tell Interactive Rebase which part of the commit history we want to edit. This is done by feeding it a commit hash: the parent commit of the one we want to manipulate.

$ git rebase -i 6bcf266b

An editor window will then open up. It contains a list of all commits after the one we provided as a basis for the Interactive Rebase in the command:

Showing the range of commits we selected for editing in our Interactive Rebase session
The range of commits we selected for editing in our Interactive Rebase session. (Large preview)

Here, it’s important that you don’t follow your first impulse: in this step, we do not edit the commit message, yet. Instead, we only tell Git what kind of manipulation we want to do with which commit(s). Quite conveniently, there’s a list of action keywords noted in the comments at the bottom of this window. For our case, we mark up line #1 with reword (thereby replacing the standard pick).

All that’s left to do in this step is to save and close the editor window. In return, a new editor window will open up that contains the current message of the commit we marked up. And now is finally the time to make our edits!

Here’s the whole process at a glance for you:

Using Interactive Rebase to edit an old commit’s message, from start to finish.

Correcting a Broken Commit (in a Very Elegant Way)

Finally, we’re going to take a look at fixup, the Swiss Army Knife of undoing tools. Put simply, it allows you to fix a broken/incomplete/incorrect commit after the fact. It’s truly a wonderful tool for two reasons:

  1. It doesn’t matter what the problem is.
    You might have forgotten to add a file, should have deleted something, made an incorrect change, or simply a typo. fixup works in all of these situations!
  2. It is extremely elegant.
    Our normal, instinctive reaction to a bug in a commit is to produce a new commit that fixes the problem. This way of working, however intuitive it may seem, makes your commit history look very chaotic, very soon. You have “original” commits and then these little “band-aid” commits that fix the things that went wrong in the original commits. Your history is littered with small, meaningless band-aid commits which makes it hard to understand what happened in your codebase.
Your commit history can be very hard to read if you constantly fix small mistakes with so-called band-aid commits
Constantly fixing small mistakes with “band-aid commits” makes your commit history very hard to read. (Large preview)

This is where fixup comes in. It allows you to still make this correcting band-aid commit. But here comes the magic: it then applies it to the original, broken commit (repairing it that way) and then discards the ugly band-aid commit completely!

Fixup applies your corrections to the original commit and then disposes of the superfluous band-aid commit
Fixup applies your corrections to the original commit and then disposes of the superfluous band-aid commit. (Large preview)

We can go through a practical example together! Let’s say that the selected commit here is broken.

Fixing the selected incorrect commit in an elegant way
The selected commit is incorrect — and we’re going to fix it in an elegant way. (Large preview)

Let’s also say that I have prepared changes in a file named error.html that will solve the problem. Here’s the first step we need to make:

$ git add error.html
$ git commit --fixup 2b504bee

We’re creating a new commit, but we’re telling Git this is a special one: it’s a fix for an old commit with the specified SHA-1 hash (2b504bee in this case).

The second step, now, is to start an Interactive Rebase session — because fixup belongs to the big toolset of Interactive Rebase.

$ git rebase -i --autosquash 0023cddd

Two things are worth explaining about this command. First, why did I provide 0023cddd as the revision hash here? Because we need to start our Interactive Rebase session at the parent commit of our broken fellow.

Second, what is the --autosquash option for? It takes a lot of work off our shoulders! In the editor window that now opens, everything is already prepared for us:

The Interactive Rebase session window
The Interactive Rebase session window (Large preview)

Thanks to the --autosquash option, Git has already done the heavy lifting for us:

  1. It marked our little band-aid commit with the fixup action keyword. That way, Git will combine it with the commit directly above and then discard it.
  2. It also reordered the lines accordingly, moving our band-aid commit directly below the commit we want to fix (again: fixup works by combining the marked-up commit with the one above!).

In short: There’s nothing to do for us but close the window!

Let’s take a final look at the end result.

  • The formerly broken commit is fixed: it now contains the changes we prepared in our band-aid commit.
  • The ugly band-aid commit itself has been discarded: the commit history is clean and easy to read — as if no mistake had occurred at all.
An example of how a clean commit history looks like
The end result after using the fixup tool: a clean commit history! (Large preview)

Knowing How to Undo Mistakes is a Superpower

Congratulations! You are now able to save your neck in many difficult situations! We cannot really avoid these situations: no matter how experienced we are as developers, mistakes are simply part of the job. But now that you know how to deal with them, you can face them with a laid-back heart rate. 💚

If you want to learn more about undoing mistakes with Git, I can recommend the free “First Aid Kit for Git”, a series of short videos about exactly this topic.

Have fun making mistakes — and, of course, undoing them with ease!

Smashing Editorial (vf, il)