Undoing in Git

„(..) great responsibility follows inseparably from great power.”
― French National Convention

Git is probably the most popular distributed version control system. When used properly, it allows a team to be extremely effective. When misused, it can create a total mess out of the code base. Every now and then you end up in a situation, when you have to undo your actions — it can include invalid file edits or deletions or even improperly executed Git commands. Git provides many ways of undoing things, including bringing back removed commits. This posts is intended to be a summary for the most common scenarios of undoing with Git.

Table of contents

  1. Restoring deleted files
    1. Deletion is not committed and not staged
    2. Deletion is not committed but has been added to the staging area
    3. Deletion was committed
  2. Undoing commits
    1. Reverting commits
    2. Deleting n last commits
    3. Deleting commits selectively
  3. Restoring “lost” commits
  4. Undoing merges
    1. Merge not finished
    2. Merge finished
  5. Undoing rebases
    1. Rebase not finished
    2. Rebase finished
  6. Reverting arbitrary patches
  7. Removing untracked files

 

Restoring deleted files

Deletion is not committed and not staged yet

We made some changes to the working tree. This includes a file deletion. git status shows the following:

The deleted file turns up to be needed, so you want to restore it. The git status command shows the solution — we have to use git checkout as follows:

Deletion is not committed but has been added to the staging area

If the deletion was added to the staging area then using just git checkout is not enough, though. In such case, we have to unstage it first with git reset and only then restore with git checkout. We specify file path after -- as follows:

File deletion was committed

Let’s assume that we want to restore a file removed a few commits ago. The commit tree looks as in the following picture:

We can perform git checkout with hash of the commit when the file existed and -- to specify the file:

Undoing commits

This section presents ways of undoing changes committed to a Git repository.

Reverting commits

We can use git revert COMMIT_HASH to revert changes made in a commit specified by the hash. This creates a new commit with reverse changes. It is shown in the picture below:

While it might be very useful to keep track of such reversions, sometimes we might want to remove a commit “silently”. It is described in the next section.

Hint: You can specify more than one commit for git revert to undo multiple commits.

Deleting n last commits

We can use git reset COMMIT_HASH to remove all commits after specified hash from the working tree. It is illustrated in the following picture:

Hint: Instead of using commit hash, you can use HEAD~n to remove n last commits.

What happens with the changes from the removed commits? By default they just remain in the working directory (will be reported as untracked files by git status). You can also decide to delete them — just add --hard option to the command. There are multiple other switches: for example --soft will put all remaining changes to the staging area.

If this is just a local branch, then that’s it. But if it is tracking a remote branch, it must be pushed with --force option, as this approach rewrites the history. This must always be done with special care as it might disorganize the work of other team members.

Deleting commits selectively

Let’s assume that we want to get rid of the commit 74eaf completely as shown in the following picture:

We can use interactive rebase for this purpose. The commit preceding our target ( 55c24) becomes the rebase point. We start the process with git rebase -i 55c24. Git then presents a text file summarizing all commits after that point:

To get rid of a commit, just remove the related line (in this case it is pick 74eaf added Person) and save the file. Git will pick remaining commits and apply them one by one.

Hint: Instead of using commit hash, you can use HEAD~n to start rebasing from n commits ago.

It is super-important to remember that this approach changes the branch history. Since changes are applied on a new base (which does not contain the deleted commit anymore), they will have different hashes:

If this is just a local branch, then that’s it. But if it is tracking a remote branch, it must be pushed with --force option, as this approach rewrites the history. This must always be done with special care as it might disorganize the work of other team members.

Amending the last commit

If you want to include additional changes in the last commit, you can use --amend switch for git commit. This will replace the last commit with a new one, which will include all the changes from the old commit as well as all the changes from the staging area. It is described in the picture below:

If this is just a local branch, then that’s it. But if it is tracking a remote branch, it must be pushed with --force option, as this approach rewrites the history. This must always be done with special care as it might disorganize the work of other team members.

Restoring “lost” commits

Some of the approaches presented in previous sections modified the branch history. It is useful to remember, though, that Git internal data structures are immutable. It means, that when you e.g. amend a commit, it is not really changed, but rather a new commit is created and put on the tip of the branch. What happens with the old commit then? If it is present in another branch, you can find it there. But what if it is not? There is still a good chance to restore it. Unless garbage collection was performed on the repository, you can still refer to the commit using its hash. The example below shows how you can cherry-pick a “deleted” commit:

What if we don’t remember the commit hash, though? We can use git reflog to see previous values of the HEAD marker. Let’s consider the following example for git commit --amend:

This log starts with the most recent events. As we can see, commit  2013ac2 has been amended. This resulted in a new commit — be1e6b5, which is the current HEAD and the current tip of testbranch.

Undoing merges

Merge not finished

If we are in the middle of a merge and encountered conflicts, we can use git merge --abort to cancel the entire operation. This will leave the commit tree and the working directory in the state from before the merge.

Merge finished

Let’s assume we have a merge which we want to undo. The situation is presented in the picture below:

Generally, we can use git reset as described in Deleting n last commits section above. In this case, git reset --hard c19d3 would work. We can use ORIG_HEAD marker as a shortcut. ORIG_HEAD points to the HEAD value before the last dangerous operation. Merge is considered dangerous, so it will point exactly to the commit which we want to switch back to. Therefore git reset --hard ORIG_HEAD is a universal way of undoing a just-finished merge.

Undoing rebases

Rebase not finished

If we are in the middle of a rebase and encountered conflicts, we can use git rebase --abort to cancel the entire operation. This will leave the commit tree and the working directory in the state from before the rebase.

Rebase finished

If no other dangerous operation has been done after the rebase, then ORIG_HEAD still points to the previous tip of the branch. You can use git reset --hard ORIG_HEAD in such case (as usual, be aware that --hard might be harmful).

If you can’t rely on ORIG_HEAD (because another dangerous operation was done after the rebase), then you can use git reflog to find the previous HEAD value. For example, consider the following reflog:

As we can see, HEAD was initialiy set to 6f5ba87, then interactive rebase was performed. After that, branch feature1 was merged. The last merge modified ORIG_HEAD, so we need to use commit hash (in this case it is 6f5ba87) to undo both the merge and the rebase.

Reverting arbitrary patches

You can use git apply to apply a patch to files in the working directory. There is a useful -R switch, which will apply reverse changes, i.e. if the patch adds a line to a file, then git apply -R will remove that line etc. This creates a many ways of effective undoing things. Let’s consider the following commands first:

  • git diff — shows all unstaged changes in the working directory as a patch
  • git diff COMMIT_1..COMMIT_2 — shows a patch which includes changes from all commits between COMMIT_1 (exclusive) and COMMIT_2 (inclusive)
  • git stash show -p  — shows stashed changes as a patch

For example, you can combine git diff with git apply -R to revert changes from your working directory:

Removing untracked files

You can remove untracked files from the working directory with git clean command. Depending on your Git configuration it might require additional flags to delete files ( -f), directories ( -d) or both ( -fd):

Summary

This post presented multiple commands which might be useful for reverting changes in a Git repository. The list is not exhaustive, but hopefully makes a useful cheat-sheet for the most common scenarios. I encourage to explore Git manual for more options. Happy coding!

More articles about Git

If you liked this post, then you might also like my The Power Of Git Interactive Rebase post, which shows (on pictures) how interactive rebase works and what you can achieve with it. I highly recommend it!

Please follow and like us:

1 response to "Undoing in Git"

Leave a Reply

Your email address will not be published. Required fields are marked *