Git: How to undo commits

Just press here to undo!

To errr is human, to undo computer can help.

In this post, we’ll look at several ways to undo Git commits:

  1. How to undo the last commit with git reset.

    Useful when you accidentally committed something you didn’t mean to, on a local branch only.

  2. How to undo previous commits with git revert.

    Handy for undoing bad commits on your main branch, without breaking history.

  3. How to rewrite history to remove previous commits with git rebase.

    Useful for reworking feature branches.

Let’s dig in!

How to undo the last commit

To undo the last commit, use git reset with @~:

$ git reset @~

@~ means “the parent of the last commit”, using the @ alias for HEAD with the ~ revision parameter syntax. So this command tells Git to reset to the state before the last commit. Git preserves the state of all files, allowing you to re-add and commit.

For example, say you had these two commits:

$ git log --oneline
94967b4 (HEAD -> soil) Add compost
7372df9 Loosen soil
...

…and you undid the last commit:

$ git reset @~
Unstaged changes after reset:
M garden.txt

…then the “Add compost” commit would have been undone:

$ git log --oneline
7372df9 (HEAD -> soil) Loosen soil
...

…and its file changes ready for you to add again:

$ git status
On branch soil
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
  modified:   garden.txt

no changes added to commit (use "git add" and/or "git commit -a")

Cool beans.

Use reset --soft to keep files staged

If you want to commit again with the same changes, it can be tiresome to re-add them. The --soft option keeps the changes staged in Git’s index, so you don’t need to git add them again:

$ git reset --soft @~

Neat. This is the git reset form that I normally use.

Say you were in the same state as before:

$ git log --oneline
54f058d (HEAD -> soil) Add compost
7372df9 Loosen soil
...

If you undo the last commit with --soft:

$ git reset --soft @~

…then the “Add compost” commit would again be undone:

$ git log --oneline
7372df9 (HEAD -> soil) Loosen soil
...

…but this time its changes would be added:

$ git status
On branch soil
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  modified:   garden.txt

Ready for you to tweak the changes and commit again.

Use reset --hard to discard changes

If the last commit is no longer relevant, use the --hard option to throw away its changes:

$ git reset --hard @~

Useful to drop temporary commits made for testing.

Going back to the same initial scenario:

$ git log --oneline
917da50 (HEAD -> soil) Add compost
7372df9 Loosen soil
...

If you use a hard reset:

$ git reset --hard @~
HEAD is now at 7372df9 Loosen soil

…then the “Add compost” commit is undone:

$ git log --oneline
7372df9 (HEAD -> soil) Loosen soil
...

…and no changes are pending:

$ git status
On branch soil
nothing to commit, working tree clean

All gone!

Undo several commits with @~<n>

You can specify the number of commits to undo with a number after the ~:

$ git reset @~<n>

This will undo the last commit “n” times at once.

Combine with --soft or --hard as appropriate.

For example, say you wanted to undo the last two commits to undo:

$ git log --oneline
68ae947 (HEAD -> soil) Add fertilizer
37b7eb0 Add compost
7372df9 Loosen soil
...

You could use @~2 to undo “Add fertilizer” and “Add compost”:

$ git reset HEAD~2
Unstaged changes after reset:
M garden.txt

…and you’d be back at “Loosen soil”:

$ git log --oneline
7372df9 (HEAD -> soil) Loosen soil
...

Bee-rilliant.

Decorative scroll.

How to undo previous commits

Undoing commits before the last one is a little trickier. Git’s history is immutable, so you can’t simply remove a commit from it. You can either create a new commit that undoes the previous one (revert), or you can rewrite history to remove the unwanted commit (rebase). We’ll look at revert in this section, and rebase in the next one.

To undo a previous commit with a new “revert commit”, use git revert with the commit’s SHA:

$ git revert <commit>

This will create a new commit that undoes the changes made in that previous commit.

Take our trusty example again:

$ git log --oneline
0f42277 (HEAD -> soil) Add compost
1c2624f Loosen soil
...

To undo “Loosen soil”, you can use its SHA with git revert:

$ git revert 1c2624f
Auto-merging garden.txt
hint: Waiting for your editor to close the file...

Git will open up your text editor to edit the commit message in the COMMIT_EDITMSG file, which has a templated default message like:

Revert "Loosen soil"

This reverts commit 1c2624f0e4900bd5b4f234025d38f0ea72498bf7.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch soil
# Changes to be committed:
# modified:   garden.txt
#

Update as you see fit and close the file. Git then makes the commit:

$ git revert 1c2624f
Auto-merging garden.txt
[soil 18d6fa8] Revert "Loosen soil"
 1 file changed, 1 insertion(+), 1 deletion(-)

…and you can see the revert commit in the log:

$ git log --oneline
18d6fa8 (HEAD -> soil) Revert "Loosen soil"
0f42277 Add compost
1c2624f Loosen soil
...

Done and undone.

Handle merge conflicts if necessary, or abort

If the changes in the previous commit have been built on, reverting the commit will cause merge conflicts. This looks like:

$ git revert bdcc361
Auto-merging garden.txt
CONFLICT (content): Merge conflict in garden.txt
error: could not revert bdcc361... Loosen soil
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git revert --continue".
hint: You can instead skip this commit with "git revert --skip".
hint: To abort and get back to the state before "git revert",
hint: run "git revert --abort".

The revert commit hasn’t yet been made. Instead, you need to resolve the conflicts in order to continue, or you can abort. The hint messages tell you the main commands you’ll need here.

If you check git status you’ll see the files that are still in conflict:

$ git status
On branch soil
You are currently reverting commit bdcc361.
  (fix conflicts and run "git revert --continue")
  (use "git revert --skip" to skip this patch)
  (use "git revert --abort" to cancel the revert operation)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
  both modified:   garden.txt

no changes added to commit (use "git add" and/or "git commit -a")

The message “You are currently reverting commit ...” will remain until you complete the revert, or abort it. You won’t be able to do many other things, like commit or switch branch, until you handle the revert.

If you manage to fix the conflicts, use git add to mark the conflicted files as ready to commit. Then run git revert --continue to complete the revert, which will again allow you to edit the commit message:

$ git add garden.txt

$ git revert --continue
[soil 3399fa4] Revert "Loosen soil"
 1 file changed, 1 deletion(-)

If you cannot fix the conflicts, you can give up on the revert with:

$ git revert --abort

…which will reset things as they were before you tried to revert:

$ git log --oneline
93cc4de (HEAD -> soil) Add compost
bdcc361 Loosen soil
...

$ git status
On branch soil
nothing to commit, working tree clean

Back to the way things were.

Use revert -n to revert without committing

If you only want to temporarily revert a previous commit’s changes, you probably don’t want to create an undo commit. You can use the -n option, short for --no-commit, to skip creating the undo commit:

$ git revert -n <commit>

This is useful for before/after testing.

Returning to this two commit setup:

$ git log --oneline
1c13e39 (HEAD -> soil) Add compost
5c87687 Loosen soil
...

You could undo “Loosen soil” without a commit like so:

$ git revert -n 5c87687
Auto-merging garden.txt

This leaves the repo in the “currently reverting” status:

$ git status
On branch soil
You are currently reverting commit 5c87687.
  (all conflicts fixed: run "git revert --continue")
  (use "git revert --skip" to skip this patch)
  (use "git revert --abort" to cancel the revert operation)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  modified:   garden.txt

In this state, you can use one of several commands to affect the revert:

  • git revert --abort will discard the revert changes, as above for handling conflicts. Handy if you want to temporarily test something with the prior commit undone, then forget about it.
  • git revert --continue will continue to create a commit. Useful if you want to check that undoing the commit has the expected effect before committing.
  • git revert --quit will forget about the revert, but leave its changes in place. This is useful if you want to further test or adjust the revert, and maybe combine it with another change.

Choose your own adventure!

Undo several previous commits

You can pass several commit references to git revert to undo them all:

$ git revert <commit1> <commit2> ...

Git will revert each one at a time, potentially pausing for a merge conflict at each step.

Once more, let’s take that three commit history:

$ git log --oneline
e0bec77 (HEAD -> soil) Add fertilizer
1c13e39 Add compost
5c87687 Loosen soil
...

You can create revert commits for both “Loosen soil” and “Add compost” by using their SHAs:

$ git revert 5c87687 1c13e39
Auto-merging garden.txt
[soil 73dc801] Revert "Loosen soil"
 1 file changed, 1 insertion(+), 1 deletion(-)
Auto-merging garden.txt
[soil 06213d6] Revert "Add compost"
 1 file changed, 1 insertion(+), 1 deletion(-)

Git will open your editor for each commit message in turn.

If you want to end up with a single revert commit, you can use -n:

$ git revert -n 5c87687 1c13e39
Auto-merging garden.txt
Auto-merging garden.txt

The repo will then be in the “currently reverting” status:

$ git status
On branch soil
You are currently reverting commit 1c13e39.
  (all conflicts fixed: run "git revert --continue")
  (use "git revert --skip" to skip this patch)
  (use "git revert --abort" to cancel the revert operation)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  modified:   garden.txt

From there, you can use any of the revert sequencer commands as necessary.

Decorative scroll.

How to rewrite history to remove previous commits

To undo a previous commit by removing it from history, you’ll need to use an interactive rebase. You’d normally only want to this on a feature branch that hasn’t been merged to your main branch.

In short, start an interactive rebase with:

$ git rebase -i main

Then, edit the rebase file to remove the target commit(s), and close it let Git complete the rebase. You may encounter merge conflicts on the way.

Par exemple, take this history:

$ git log --oneline
1c13e39 (HEAD -> soil) Add compost
5c87687 Loosen soil
07a4556 (main) Set up planter
...

There are two commits on the current branch soil, which is branched off main.

You can start the rebase onto main with:

$ git rebase -i main
hint: Waiting for your editor to close the file...

Git pops open your editor with the rebase plan:

pick 5c87687 Loosen soil
pick 1c13e39 Add compost

# Rebase 07a4556..1c13e39 onto 07a4556 (2 commands)
#
# Commands:
...

To remove a commit from history, you can simply delete its line. (Or replace “pick” with the “d” (“drop”) command, if you want to be explicit.)

After you save and close the file, Git will continue:

$ git rebase -i main
Successfully rebased and updated refs/heads/soil.

…and the log now shows only the retained commit:

$ git log --oneline
bd86d96 (HEAD -> soil) Add compost
07a4556 (main) Set up planter

Bada-bing, bada-boom.

Note that its SHA has changed (from 1c13e39 to bd86d96). This is because it has been recreated. Git’s history is immutable, so rebasing recreates commits, reusing the same message, authorship, and changes, but new parents.

Handle merge conflicts if necessary, or abort

If the changes in the previous commit has been built on, removing the commit in a rebase will cause merge conflicts. In this case, Git will stop the rebase part way to tell you:

$ git rebase -i main
Auto-merging garden.txt
CONFLICT (content): Merge conflict in garden.txt
error: could not apply 02b3262... Add compost
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 02b3262... Add compost

Don’t panic!

The hint messages summarize the actions you can take.

git status will show you the files that have conflicts, and further hints:

$ git status
interactive rebase in progress; onto 07a4556
Last command done (1 command done):
   pick 02b3262 Add compost
No commands remaining.
You are currently rebasing branch 'soil' on '07a4556'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
  both modified:   garden.txt

no changes added to commit (use "git add" and/or "git commit -a")

As with a conflicted revert, this is a special state. You can’t do many things, like switch branch, until you finish or abort the rebase.

Your main options are:

  • git rebase --continue to complete the rebase, after you fix the conflicts and git add the affected files.

    This option is mostly likely what you want to do.

    For example:

    $ open garden.txt
    
    $ git add garden.txt
    
    $ git rebase --continue
    [detached HEAD 48a4936] Add compost
     1 file changed, 7 insertions(+)
    Successfully rebased and updated refs/heads/soil.
    
  • git rebase --skip to skip the current commit.

    This is rarely useful, as it means the resulting history won’t have all the commits you selected during rebase. In the example, this would remove the “Add compost” commit, leaving the branch without any commits.

  • git rebase --abort to give up on the rebase.

    This will reset your repo to the way things were before rebasing, with the full commit history, including the commit you attempted to drop.

…and that’s a wrap!

Decorative scroll.

Fin

May your undoes always go smoothly, and not needing undoing themselves,

—Adam


Read my book Boost Your Git DX for many more Git lessons.


Subscribe via RSS, Twitter, Mastodon, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: