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).
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.
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.
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
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
$ 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
~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.
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! 😱
This is a classic case for Git’s Reflog tool! Let’s see how it can save your neck:
$ git reflog
Let’s decompose this bit by bit:
- First of all, the Reflog is very easy to open: a simple
git reflogis 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
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 master
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 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.
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.
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
$ git checkout feature/newsletter
Now we can safely move that commit over using the
$ git cherry-pick 26bf1b48
We can take a look at
feature/newsletter and will see that, now, the commit exists here as well.
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!