How to undo commits in Git?

you should have Basic knowledge of Git to do so see this: What is Git? How to Use Git?

Table of content:

Introduction:

What if you accidentally committed the wrong files to Git, but haven’t pushed the commits to the server yet.
Is it POSSIBLE to undo those commits from the local repository?

your answer is Yes!
We will explain a few different techniques to undo your changes based on the stage of the change in your current development. Also, keep in mind that nothing in Git is ever deleted.
This means that it is still possible to view your detached commits with git reflog command and access them with direct commit ID.
they will continue to be available until Git automatically cleans these detached commits.


Quickly Save Local changes:

You can discard all local changes, but save them for possible re-use later using:

git stash

Or, you can discard local changes (permanently) to a file using:

git checkout -- <file>

Or, you can easily discard all local changes to all files permanently using:

git reset --hard

note:
git stash is very recommended since you are able to keep your work saved and change to another branch and do some other work. Then use git stash pop to continue where you left.
Here are other git lash commands:

  • git stash save, it allows you to add a message that helps identify your commit, plus other options.
  • git stash list, it lists all previously stashed commits that were poped using git stash pop.
  • git stash pop, which redoes previously stashed changes and removes them from stashed list
  • git stash apply, it redoes previously stashed changes(stashed changes are kept in the list).

Undo Local changes:

Local changes can be on various stages and each stage has a different approach on how to undo them, these main stages are: before committing, after committing, after pushing to a remote repository.
When a change is made. Git proposes a solution to discard changes to a certain file.
Suppose you edited a file to change the content using your favorite editor:

vim <file>

in this case, you either added your files to be staged or not, to be tracked or not if you just created them.
you check the status of your files, using this command:

git status
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   <file>
no changes added to commit (use "git add" and/or "git commit -a")

if the above was the case then you haven’t committed your files Yet.

Next, you will learn how to undo changes before and after committing.


Undo changes Before committing in the staged and unstaged state:

at this point, you have probably added your target files to your local repository using:

vim <file>
git add <file>

you can check the status of your files whether they were actually added using:

 git status

the result would be something like this:

    $ git status
    On branch master
    Your branch is up-to-date with 'origin/master'.
    Changes to be committed:
      (use "git reset HEAD <file>..." to unstage)

      new file:   <file>

Now you can use one of these commands to undo what you have done:

  • you can unstage the file to current commit (HEAD):

    git reset HEAD <file>
    
  • or you could unstage everything - retain changes:

    git reset
    
  • or we have already seen, you can Discard all local changes, but save them for later:

     git stash
    
  • or easily Discard everything permanently:

    git reset --hard
    

Undo changes After committing local changes:

Without history modifications(everyone can continue doing their work):

After committing, all your changes are recorded by the version control system.
And since you haven’t pushed them to a remote repository your changes are Private, and can not be viewed by other developers.

Let’s assume there are 4 commits in our example A, B, C, D. and B is the one who is Buggy or you just want to delete it.

Here is how to do it:

  • you can undo changes introduced by commit B (swap additions and deletions):

    git revert commit-B-id
    
  • Undo changes on a single file or directory from commit B, but retain them in the staged state:

    git checkout commit-B-id <file>
    
  • Undo changes on a single file or directory from commit B, but retain them in the unstaged state:

    git reset commit-B-id <file>
    
  • and in case of buggy commits you should consider creating a new branch
    for instance, checkout the commit B and create a new branch and do commit E`.

    git checkout commit-B-id
    git checkout -b new-path-of-feature
    # Create <commit E>  
    git commit -a 

note:
in this case, resetting to B and do commit E is not recommended. (it will cause problems with pushing and if forced pushed also with other developers).
and using the method of creating a new branch will let everyone else do their work. and you can merge it back in later.

With History modifications:

There is only one command for history modification and that is git rebase.
Using this command with the -i flag allows you to:

  • reword commit messages (there is also git commit --amend for editing last commit message).
  • edit the commit content (changes introduced by commit) and message.
  • squash multiple commits into a single one, and have a custom or aggregated commit message.
  • drop commits - simply delete them.
    and a few more options.

note:
To perform the actions above let’s use the same example above where you have four commits A, B, C, D. and you want to delete B.
in this case use:

git rebase -i A

open your editor write drop in front of commit B, but default pick with all other commits. Save and exit the editor to perform a rebase.

But if you want to Edit B do this:

Rebase the range from current commit D to A.

git rebase -i A

In this case, you need to do the same as Deleting B, you only need to write edit instead of drop.

Redoing an Undo in Git:

Yeah, In some cases you realize you should never undo a change whether it was by mistake or not.
Luckily, this same command can solve that problem:

 git reflog

git reflog enables you to recall detached local commits by referencing or applying them via commit ID.
note:
Git regularly cleans the commits which are unreachable by branches or tags.
So don’t expect it to see really old commits in the reflog!
Here is an example of tracking old commits:

    $ git reflog show

    # Example output:
    b673187 [email protected]{4}: merge 6e43d5987921bde189640cc1e37661f7f75c9c0b: Merge made by the 'recursive' strategy.
    eb37e74 [email protected]{5}: rebase -i (finish): returning to refs/heads/master
    eb37e74 [email protected]{6}: rebase -i (pick): Commit C
    97436c6 [email protected]{7}: rebase -i (start): checkout 97436c6eec6396c63856c19b6a96372705b08b1b
    ...
    88f1867 [email protected]{12}: commit: Commit D
    97436c6 [email protected]{13}: checkout: moving from 97436c6eec6396c63856c19b6a96372705b08b1b to test
    97436c6 [email protected]{14}: checkout: moving from master to 97436c6
    05cc326 [email protected]{15}: commit: Commit C
    6e43d59 [email protected]{16}: commit: Commit B

the first column (e.g : b673187), represents the commit ID.
Where the following column, the number next to HEAD (e.g{5}), indicates how many commits ago something was made.
afterward, the indicator of the action that was made (commit, rebase, merge, …), and then, in the end, the description of that action.


Undo changes pushed to the repository:

Without changing history

Undoing changes without changing history is the most preferred way of undoing changes in any remote repository or public branch.
The best way to do so has always been Branching ever since you want to retain the history of faulty development, yet start anew from a certain point.

Branching benefits:

  • provides a clear timeline.
  • provides a clear and simple to understand development structure.
  • enables you to include the existing changes in new development using merging.

How to revert changes in certain commit-id?

revert changes introduced in certain commit-id

  • If you want to revert changes introduced in certain commit-id you can simply revert that commit-id (swap additions and deletions) in newly created commit: You can do this with:
      git revert commit-id
    
  • You can create a new branch:
    git checkout commit-id   
    git checkout -b new-path-of-feature
    

Undo commits with modifying History:

Warnings:

→ Never modify the commit history of master or shared branch!
→ Modified history breaks the development chain of other developers!

keep in mind that, even with modified history, commits are just detached and can still be accessed through commit ID

How to undo commits with modifying History:

After setting what to modify (how far in history or which range of old commits), use:

 git rebase -i commit-id

This command displays all the commits from the current version to the chosen commit ID and then allows modification, squashing, deletion of those commits.

$ git rebase -i commit1-id..commit3-id
pick <commit1-id> <commit1-commit-message>
pick <commit2-id> <commit2-commit-message>
pick <commit3-id> <commit3-commit-message>

# Rebase commit1-id..commit3-id onto <commit4-id> (3 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Warnings:

→ It is important to notice that comment from the output clearly states that, if you decide to abort, then do not just close your editor (as that will in-fact modify history), but remove all uncommented lines and save.

→ git rebase should be used carefully on shared and remote branches. But don’t worry, there will be nothing broken until you push back to the remote repository (so you can freely explore the different outcomes locally).

 # Modify history from commit-id to HEAD (current commit)
 git rebase -i commit-id

delete Sensitive information from commits:

first, you need to run git filter-branch.
this enables you to rewrite history with certain filters. This command uses rebase to modify history and if you want to remove a certain file from history altogether use:

git filter-branch --tree-filter 'rm filename' HEAD

Note:
git filter-branch command might be slow on big repositories!
That’s why there are tools that can use some of Git specifics to enable faster execution of common tasks (which is exactly what removing sensitive information file is about).

An alternative is the open-source community-maintained tool BFG.
Since these tools focus on specific use cases they can be much faster.