/ git

git commit accidents - how to undo one

So you've just pushed your local branch to a remote branch, but then realized that one of the commits should not be there, or that there was some unacceptable typo in it. No problem, you can fix it. But you should do it rather fast before anyone fetches the bad commits, or you won't be very popular with them for a while ;)

If you need to keep the history of your repository intact, you basically have two options (with a third workaround):

Option 1: Update your repository with a new commit

For a simple correction in one, or even a handful, of file, simply remove or fix the bad file(s) in a new commit and push it to the remote repository. This is the easiest (and non-destructive) way to correct an error, and should suffice in most cases. This way your original (bad) commit will remain but you will have a complete history.

Option 2: "Revert" the commit

If you need to back out every single change in all files in a single commit, you can do this easily by executing a revert.

This is a good alternative to the first option above, when you just need to do a bulk undo of the previously (or at any point in the past) committed changes.

Reverting a commit means to create a new commit, that undoes all changes made in the commit you are reverting.

As with option 1, your history will remain intact.

To do this simple specify the revert command, noting the hash of the commit you wish to undo. Git will handle the rest:

$ git revert 7c63649

Option 3 - Rewrite History

It should be noted that rewriting history can wreak havoc on other users if you have a very large developer base all referencing your repository. Especially if it is highly active and people rely on staying up to date... You should generally avoid history rewriting for this reason. Your new version of history cannot be pulled like would normally be the case. If other users are referencing commits in the future of your newly created view of the code-base, they will have some serious work on their hands if they need to merge changes already completed in their local repositories.

However, sometimes you do want to rewrite the history. Be it because of leaked sensitive information, to get rid of some very large files that should not have been there in the first place, or just because you want a clean history (I certainly do).

I usually also do a lot of very heavy history rewriting when converting some repository from Subversion or Mercurial over to Git, be it to enforce internal LF line endings, fixing committer names and email addresses or to completely delete some large folders from all revisions. I recently also had to rewrite a large git repository to get rid of some corruption in an early commit that started causing more and more problems.

Yes, you should avoid rewriting history which already passed into other forks if possible, but the world does not end if you do nevertheless. For example you can still cherry-pick commits between the histories, e.g. to fetch some pull requests on top of the old history.

In open-source projects, always contact the repository maintainer first before doing any history rewriting. There are maintainers that do not allow any rewriting in general and block any non-fast-forward pushes. Others prefer doing such rewritings themselves.

Case 1: Delete the last commit

Deleting the last commit is the easiest case. Let's say we have a remote origin with branch master that currently points to commit dd61ab32. We want to remove the top commit. Translated to git terminology, we want to force the master branch of the origin remote repository to the parent of dd61ab32 (x^ points to the parent of x):

$ git push origin +dd61ab32^:master

Where git interprets hhhhhhh^ as the parent of the commit you wish to reverse and + as a forced non-fast-forward push. If you have the master branch checked out locally, you can also do it in two simpler steps: First reset the branch to the parent of the current commit, then force-push it to the remote.

$ git reset HEAD^ --hard
$ git push origin -f

Case 2: Delete the second last commit

Let's say the bad commit dd61ab32 is not the top commit, but a slightly older one, e.g. the second last one. We want to remove it, but keep all commits that followed it. In other words, we want to rewrite the history and force the result back to origin/master. The easiest way to rewrite history is to do an interactive rebase down to the parent of the offending commit:

$ git rebase -i 7c63649^

This will open an editor and show a list of all commits since the commit we want to get rid of:

pick dd61ab32
pick dsadhj278

Simply remove the line with the offending commit, likely that will be the first line (vi: delete current line = dd). Save and close the editor (vi: press :wq and return). Resolve any conflicts if there are any, and your local branch should be fixed. Force it to the remote and you're done:

$ git push origin -f

Case 3: Fix a typo in one of the commits

This works almost exactly the same way as case 2, but instead of removing the line with the bad commit, simply replace its pick with edit and save/exit. Rebase will then stop at that commit, put the changes into the index and then let you change it as you like. Commit the change and continue the rebase (git will tell you how to keep the commit message and author if you want). Then push the changes as described above. The same way you can even split commits into smaller ones, or merge commits together.