5 Ways to Undo Mistakes with Git

    Tobias Günther

    No matter how experienced you are, the craft of software development cannot be practiced without making mistakes. But what separates the average from the great programmers is that they know how to undo their mistakes!

    If you’re using Git as your version control system, you already have a host of “undo tools” at hand. This post will show you five powerful ways to undo mistakes with Git!

    Discarding Some of Your Local Changes

    Coding is often a messy process. At times, it feels like you’re taking two steps forward and one step back. In other words: some of the code that you’ve produced is great … but some of it not so much. Here’s where Git can help you: it allows you to keep what’s good and discard the changes that you don’t want anymore.

    Let’s take a look at an example scenario with some “local” changes (aka changes we haven’t committed, yet).

    Local, uncommitted changes — some of which we want to discard

    Note: for a better overview and clearer visualization, I’m using the Tower Git desktop client in some of my screenshots. You don’t need Tower to follow along with this tutorial.

    Let’s tackle the problem in general.css first. The changes we made went in a totally wrong direction. Let’s undo all of them and recreate the last committed state of that file:

    $ git restore css/general.css

    Note that, alternatively, I could have used the git checkout command to achieve the same outcome. But because git checkout has so many different jobs and meanings, I strongly prefer the slightly newer git restore command (which is focused solely on these types of tasks).

    Our second problem in index.html is a little bit more tricky. Some of the changes we made in this file are actually great, while only some of them need to be undone.

    Undoing only some of the changes in a file, while keeping others intact

    Again, git restore comes to the rescue — but this time with the -p flag, because we want to go down to the “patch” level:

    $ git restore -p index.html

    Git will then take you by the hand and ask you — for every chunk of changes in that file — if you want to discard it or not.

    Git asks us what to do with each chunk of changes

    You’ll notice that I typed “n” for the first chunk (in order to keep it) and “y” for the second chunk (in order to discard it). After the process is finished, you can see that only the first, valuable chunk of changes survived — just like we wanted!

    Resetting a Specific File to a Previous State

    Sometimes you’ll want to restore a specific file to specific revision. For example, you know that index.html was working fine at some earlier point in time, but now it’s not. That’s when you want to turn back time, but only for this specific file and not for the whole project!

    The first thing we have to find out is which exact revision we want to restore. With the right set of parameters, you can get the git log command to show you the history of just our single file:

    $ git log -- index.html

    Inspecting the commit history of a specific file

    This shows us only the commits where index.html has been changed, which is pretty helpful to find the “bad apple” revision that broke things.

    If you need more information and want to look at the contents of those commits, you could have Git show you the actual changes in these commits with the -p flag:

    $ git log -p -- index.html

    Once we’ve found the bad commit where we broke our lovely little file, we can go on and fix the mistake. We’ll do that by restoring the file at the revision before the bad one! This is important: we don’t want to restore the file at the commit which introduced the error, but at the last good state — one commit before that!

    $ git checkout <bad-commit-hash>~1 -- index.html

    Appending ~1 to the bad commit’s hash will instruct Git to do exactly that: go one revision before the referenced one.

    After executing that command, you’ll find index.html to be modified in your local working copy: Git restored that last good revision of the file for us!

    Recovering a Lost Revision with the Reflog

    Another great undo tool in Git’s first aid kit is the “Reflog”. You can think of it as a journal where Git protocols all of the HEAD pointer movements that happen in your local repository — things like commits, checkouts, merges and rebases, cherry-picks, and resets. All of the more interesting actions are nicely logged in here!

    Such a journal, of course, is perfect for those occasions when things go south. So let’s start by causing a little catastrophe — which we can then repair with the Reflog.

    Let’s say you were convinced that your last few commits were no good: you wanted to get rid of them and therefore used git reset to go back to a previous revision. As a result, the “bad” commits disappeared from your commit history — just as you wanted.

    Realizing we've made a horrible mistake — after we've pulled the trigger on the “git reset” command

    But as life sometimes goes, you notice that this was a bad idea: in the end, the commits weren’t so bad after all! But the bad news, of course, is that you’ve just wiped them from your repository’s commit history! 😱

    Using the Reset command to restore a previous state — and thereby “delete” newer commits from the history

    This is a classic case for Git’s Reflog tool! Let’s see how it can save your neck:

    $ git reflog

    The Reflog gives us a nice overview of our most recent actions

    Let’s decompose this bit by bit:

    • First of all, the Reflog is very easy to open: a simple git reflog is all you need.
    • Second, you’ll notice that the logged states are sorted in chronological order, with the newest one at the top.
    • If you look closely, you’ll see that the topmost (that is, newest) item is a “reset” action. Which is what we just did 20 seconds ago. Apparently, the journal works 😉
    • If we now want to undo our involuntary “reset”, we can simply return to the state before — which is also neatly documented here! We can simply copy the commit hash of that previous state to the clipboard and take it from there.

    To restore this previous state, we can either use git reset again or simply create a new branch:

    $ git branch happy-ending e5b19e4

    We were able to restore the branch and the valuable commits we thought we had lost

    As we can happily verify, our new branch contains the commits we thought we had lost through our accidental git reset hiccup!

    Recovering a Deleted Branch

    The Reflog can come in handy in other situations as well. For example, when you’ve inadvertently deleted a branch that you really (!) shouldn’t have. Let’s take a look at our example scenario:

    $ git branch
      * feature/analytics

    Let’s pretend that our customer / team lead / project manager tells us that the beautiful analytics feature we’ve been working on isn’t wanted anymore. Tidy as we are, we delete the corresponding feature/analytics branch, of course!

    Above, you can see that, in our example scenario, we have that branch currently checked out: feature/analytics is our current HEAD branch. To be able to delete it, we have to switch to another branch first:

    $ git checkout master
    $ git branch -d feature/analytics
    error: The branch 'feature/analytics' is not fully merged.
    If you are sure you want to delete it, run 'git branch -D feature/analytics'.

    Git tells us that we’re about to do something really serious: since feature/analytics contains unique commits that are present nowhere else, deleting it will destroy some (potentially valuable) data. Well … since the feature isn’t needed anymore, we can go on:

    $ git branch -D feature/analytics
    Deleted branch feature/analytics (was b1c249b).

    You probably already anticipated what’s coming now: our customer / team lead / project manager happily tells us that the feature is back in the game! 🥳 They want to pursue it after all! 🎉

    Again, we’re faced with an ugly situation where we might have lost valuable data! So let’s see if the Reflog can save our neck one more time:

    $ git reflog

    The Reflog shows the problem — and the solution

    The good news becomes apparent if you take a closer look. Before we were (disastrously) able to delete our branch, we had to perform a git checkout to switch away from it (since Git doesn’t allow you to delete the current branch). Of course, this checkout was also logged in the Reflog. In order to restore our deleted branch, we can now simply take the state before as a starting point for a new branch:

    $ git branch feature/analytics b1c249b

    And voila: our branch is back from the dead! 🌸💀🌺

    If you happen to use a desktop GUI like Tower, undoing mistakes like these is often as simple as pressing CMD + Z.

    A desktop GUI for Git can often help and make undoing things easier

    Moving a Commit to a Different Branch

    Let’s close with another true classic from the “Oh no!” department: committing on the wrong branch.

    Today, many teams have a rule in place that forbids committing directly to long-running branches like “main”, “master” or “develop”. Typically, new commits should arrive on those branches only through merges/rebases. And yet, we sometimes forget and commit directly …

    Let’s take the following scenario as an example.

    A classic case of a commit that happened on the wrong branch

    We should have committed on feature/newsletter, but inadvertently pulled the trigger on the master branch. Let’s look at the solution, step by step.

    First, we have to make sure we’re on the right branch this time, so we’re checking out feature/newsletter:

    $ git checkout feature/newsletter

    Now we can safely move that commit over using the cherry-pick command:

    $ git cherry-pick 26bf1b48

    We can take a look at feature/newsletter and will see that, now, the commit exists here as well.

    The commit has been moved to the correct branch, thanks to the cherry-pick command

    So far, so good. But cherry-pick did not remove the commit from the master branch — where it should never have been. So we’ll have to clean up our mess there, too:

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

    We’re switching back to master and then using git reset to remove the (here) unwanted commit from the history.

    Finally, all is well again — so you may start your happy dance now! 💃🕺🏻

    Undoing Mistakes is a Superpower

    I’ve said it before, and unfortunately we all know it’s true: we cannot avoid mistakes! No matter how great a programmer we are, we’ll mess up from time to time. The question, therefore, is not if we make mistakes, but rather how well we can deal with them and fix the problems?

    If you want to learn even more about Git’s undo tools, I highly recommend the “First Aid Kit for Git”. It’s a (free) collection of short videos that show you how to clean up and undo mistakes in Git.

    Have fun moving fast, breaking things — and of course, repairing them!