git revert <HASH>undoes a commit: it commits the inverse of the change done by that commit. Concretely, it's just like a cherry-pick but with a backwards commit. (In git's implementation, the two are implemented by the same file.) How's it work?
The conceptually-simplest way to roll back is to apply an inverse patch, something like:
git diff HASH HASH^ | patch
but that does not work quite right in the presence of complicated operations, like deletes or adds. And if there are conflicts it'd be nice if all of that conflicting information known to git, so it could assist with running merge tools.*
Instead, consider a tree like this, where newer commits are on the right:
Suppose B is the commit we don't want anymore.
Conceptually, you want to go from the state in B back to the state in A, but with all of the stuff in the middle up to HEAD still kept around. Imagine you had created a new branch off of B that contained the state of the world in A, like this:
A---B---...---HEAD \ AIn that world, the rollback would just be merging that branch into HEAD.
To do this without creating branches, you can just directly run the merge command, which takes a base branch and a set of other branches to merge.
git merge-recursive B -- HEAD A
That says: "using B as the reference, merge as if HEAD and A were our interesting branches.".
More commonly, you've got the hash of a single interesting commit so it's instead:
git merge-recursive H -- HEAD H^
(Note: I'm not certain this is exactly correct; it depends on whether merge-recursive actually looks at the relationship between the branches for doing the merge. I wonder if it may be more correct to use
git read-tree -m A B HEADto read in all three trees into the index and then use
git merge-indexbut both of those are plumbing commands I don't fully understand.)
* You could use
git applyinstead of patch to help with that, but that is not the subject of this post.