I've said it before, and it's worth repeating: Git is undo on steroids! As a developer, wanting to undo changes already committed is commonplace during development. But considering Git's core concept of using immutable snapshots, undoing changes requires some thought-work and can feel daunting to begin with.
In this post we'll look at three common reasons for wanting to undo changes in Git, and look at the specific commands
restore to decide what to use when.
3 common reasons for undoing changes in Git
Whenever you get into a development flow, it's easy to unintentionally cut corners to keep the momentum going. Sometimes this leads to errors being persisted in your Git history by accident, which then need to be corrected (no one wants to look like a fool in the eyes of fellow peers, right?). Below are three pretty common cases you've probably encountered yourself if you've been developing for a while.
➕ Unintentionally adding files to the Staging Area (thought you could just
$ git add . all files in one go to save time, not realising you didn't want to stage that server log or binary?)
🌴 Accidentally making multiple commits on the wrong branch (on a streak, plowing ahead, thinking you're working in a topic branch when in fact you're still on master? – been there, done that!)
🐛 Discovering a bug in a commit done way back (only noticing that the minor change to your project's config file caused ripple effects much later?)
As with any development practice, problems like these can be solved in different ways using different techniques. But let's look at how the above issues can be solved using
restore, starting with a quick glimpse at how the official documentation describes the commands.
How reset, restore and revert differs
In Git, there are three commands with similar names:
revert; all which can be used to undo changes to your source code and history, but in different ways!
From the official documentation the options are described like this:
resetis about updating your branch, moving the tip in order to add or remove commits from the branch. This operation changes the commit history."
restoreis about restoring files in the working tree from either the index or another commit. This command does not update your branch. The command can also be used to restore files in the index from another commit."
revertis about making a new commit that reverts the changes made by other commits."
Ok, still not crystal clear? Let's contextualize the above statements and solve our initial problems using them.
Undoing changes in Git
With the general command description from above top of mind, let's see how they differ in reality.
➕ Removing an accidentally staged file (restore)
Staging all files using
$ git add . isn't something I'd recommend doing, as it's generally better to be more selective in what files to stage. Not doing so might cause you to add files by accident which you don't want to commit. If, and when, that happens,
restore is your weapon of choice.
restore has a multitude of use cases, in its simplest form it's used to remove changes from the Staging Area, or to discard changes made to your Working Tree. And just like the official documentation states,
restore doesn't update your branch (i.e. doesn't modify your commit history).
Take a look at the below example illustrating a dirty Working Tree including changes staged for commit. Performing restore on the staged changes would only undo what's about to be committed and not infer any loss of data, as the change itself is also present in the Working Tree (notice the "bar" line in index.js). On the other hand, restoring changes made to the Working Tree would ultimately discard them completely.
A cautionary warning: Whenever you restore a file inside your Working Tree, do note that this is a destructive operation leading to potential loss of data!
🌴 Removing commits from a branch (reset)
Being on a development streak (aka having a flow) is a highly appreciated feeling, typically appearing when you know exactly what to do and only need to translate your internal thought-out solution into code. However, having a flow can also unintentionally make you cut corners, for example forgetting to switch to a topic branch before doing all the work, leaving your newly crafted solution hanging in the wrong branch.
Consider the illustration below, where two commits have been committed to master (since it was last synched), when in fact the desired outcome was to do the work on a separate topic branch! As none of the local commits (C1 & C2) have yet been pushed, solving this problem is work for
reset. Before we get going resetting master we must first make sure to capture our commits in the intended topic branch, so we don't accidentally lose any work as
reset modifies our history.
With a clean Working Tree and no staged changes, first creating our topic branch (feature-1) and then resetting master is a breeze! Below is the final state following our two operations.
With master reset to its initial state, our new topic branch feature-1 is ready to be pushed remotely.
A cautionary warning: Only reset branches this way whenever you have a clean Working Tree as
$ git reset --hard is a destructive operation to any unsaved work, as it resets both your Working Tree and Staging Area.
🐛 Undoing an entire commit from way back (revert)
Most projects make use of configuration files where for example endpoints to different environments are kept. Changes to these files are generally done on a low cadence and their implications are sometimes difficult to spot at first sight. This can cause bugs to sneak into your codebase only to be discovered at a much later stage, when new work has already been based on top. When this happens,
revert can be used to solve it.
Consider the illustration below, where a bug has been discovered far back in our history (C1), and new work has already been based on it. Rewriting the entire history is probably not such a good idea, but reverting the offending commit might be a better option; essentially creating a new commit at the tip of the branch by "rolling forward", reverting any changes made by the offending commit.
As we can see above, by reverting commit C1 at the tip of master, all changes in C1 are essentially undone in C6 and history is moved forward.
You should now have a better understanding of how reset, restore, and revert differs – and when to use what! But let's remind ourselves one more time.
resetupdates your branch, moving the tip in order to add or remove commits from it. As reset updates the commit history, be careful when manipulating already published commits!
restoremanipulates either Working Tree or Staging Area by restoring files to predefined states (typically a previous version). Restore doesn’t update your history.
revertcreates a new commit that reverts changes made by another commit. It's essentially a reversed
cherry-pick, rolling your history forward.
As mentioned in the introduction, there are other ways to solve problems like these. And chances are you'll most likely encounter more complex situations where these alternatives fall short, for example if you want to undo changes inside of multiple commits. Undoing changes in a more complex manner is clearly beyond the scope of this post, but let's just say there's a very powerful alternative called interactive rebase which will be covered in a consecutive post!
😎 Thanks for reading and good luck improving your source code management skills!
If you'd like more pieces like this, make sure to subscribe to the news feed so you don't miss anything!
Any questions or suggestions, try reaching me on Twitter – @Stjaertfena