Want to see the full-length video right now for free?
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.
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.
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.
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.
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.
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.
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.
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 .
!
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^'
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.