Video

Want to see the full-length video right now for free?

Notes

For most of us, Git is a tool that we'll use constantly throughout the day, and this means that mistakes will happen. Typos in commit messages, forgetting to add files, committing to the wrong branch are all things I do more often than I'd like to admit.

Luckily, Git has great support for undoing or revising our actions, but as with so many things in Git, this functionality is hidden behind a complex and sometimes inconsistent command interface.

In this video we'll learn how to undo just about anything in Git, and how to automate and simplify these actions so they're easy to run whenever we need them.

Amending Commits

One type of operation that can often benefit from undoing or redoing in Git is committing. We'll make a handful of changes, stage, and commit, only to realize we forgot to stage one of the files. Now we should never change any history that has been published (pushed up to GitHub or merged into master), but as long as we're working in our local feature branches, it's fine to clean up these little mistakes.

Including Forgotten Files

Often we'll find ourselves in a situation where we've made some changes, staged, and committed, only to realize we forgot to add a file that was part of the change. For example, we add a gem to the Gemfile, bundle, and then stage and commit the Gemfile, but forget about the Gemfile.lock.

Thankfully Git makes it easy to fix this and get the commit we wanted in the first place using the --amend flag to the commit command. We simply stage the changes, then run commit --amend.

$ git add Gemfile.lock
$ git commit --amend --no-edit

This will take the currently-staged changes and incorporate them into the previous commit, reusing the existing commit message thanks to the --no-edit flag.

Amend Commit Message

Much of the time we'll want to use the combination of --amend and --no-edit to simply include our changes, but in some cases we specifically want to edit the commit message to fix a typo or similar. For this case, even with no changes staged, we can run

$ git commit --amend

This will open our editor with the commit message already populated. We can now edit as needed, and quit and save when ready. Git will then create a new commit that reuses the code from the previous version of our commit, but containing our updated commit message.

NOTE In both of the above cases Git creates new commit objects when we amend. Git never alters the contents of a commit, but instead creates new ones, updating where banches point and the like. We'll talk more about this in a later video, but for now it's enough to know that Git remembers your old commits, and you can restore them any time you need.

Amend No Edit Alias

While the single argument --amend is easy enough to remember, you may find the commit --amend --no-edit a bit much to remember. Luckily, we can use Git's configuration system to save it away as an alias. Run the following in your terminal

$ git config --global alias.car 'commit --amend --no-edit'

This creates an alias called car (short for "commit amend reuse" as it "reuses" the commit message) that can be used in place of the more verbose command.

Unstaging Files

Occasionally we'll find that after making a round of changes, we realize that some of the changes are entirely unrelated to others. Perhaps we've run git add ., but we then realize that we only want to commit two of the three files that are now staged.

Once again, we can use a simple Git command to undo this.

Here's how to unstage a file:

$ git status --short
 M Gemfile
 M Gemfile.lock
 A TODO.md

$ git reset TODO.md

The reset command will undo the staging of the TODO.md file, allowing us to selectively commit the changes to just the Gemfile and Gemfile.lock.

Unstage Alias

While this is a great tool to have in our Git command knowledge arsenal, it is also a great example of where the Git command interface does not always align with our mental model of the operations we are performing.

We want to "unstage" a file. reset happens to be the mechanism, but it is far from intuitive. Thankfully, once again we can create an alias. In this case, the alias exists not to capture a complex collection of options and flags, but simply to map the operation to a more familiar and intuitive name.

$ git config --global alias.unstage 'reset'

Now we can run git unstage or git unstage TODO.md and it will work as expected.

Checkout to Undo Changes

If we ever make changes to a file but then realize we actually don't want those changes, we can use checkout to revert things to how they looked in the most recent commit on our current branch.

To checkout all files, essentially resetting to how the code looked in the last commit, we can use:

$ git checkout .

NOTE This is one of the few dangerous operations in Git. If you run checkout without staging or committing your changes, Git will destroy your work and you will not be able to get it back. Be sure to use caution with git checkout .!

Undoing a Commit

Similar to unstaging, we occasionally may want to undo an entire commit. To clarify, when we say "undo a commit", we mean remove that commit from our history, and revert to the point at which the files were staged (with the changes captured in the commit).

The command used to undo a commit is:

git reset --soft HEAD^

To break this down, HEAD refers to the current branch, and HEAD^ means one commit back, aka the "parent commit". The --soft here specifies that we should reset the branch (to point at that parent commit), but otherwise leave the files in the working directory and the index untouched.

This has the effect of undoing the commit, taking us back to just before we made it. Note that it leaves our changes staged in the index.

While useful, this is a complex command to remember, so once again we can create an alias, mapping this operation to the intuitive "uncommit" alias name.

$ git config --global alias.uncommit 'reset --soft HEAD^'

Canceling an Edit

The final bit of undo magic we want to bring into our Git workflow is the ability to abort the creation of a commit in progress. Typically the last step in creating a commit (or performing a merge or rebase) is to edit the commit message in our editor. Specifically, Git is waiting for the editor to exit with a exit code of 0 to signify success.

In Vim, we can short circuit this process by using the :cquit command to quit Vim with non-zero exit code and cancel the edit. Git will then abort the operation.

Vim being Vim, :cq is sufficient for the editor to do what you mean.