Be careful with the word "revert"
When people say "I want to revert" in Git they sometimes mean what git revert
does, which is more of a back out operation, and sometimes mean what you do, which is to restore the source base from an earlier version.
To illustrate, suppose we have a commit that has just one file, README
, and three commits:
A <-B <-C <-- master (HEAD)
The version of README
in revision A says "I am a README file", and is just one line long.
The version of README
in revision B says "I am a README file." as before, but has a second line added, "This file is five lines long."
The version of README
in revision C is corrected in that its second line says "This file is two lines long."
Git's git revert
can undo a change, so that, right now, running git revert <hash-of-B>
will attempt to remove the added line. This will fail since the line doesn't match up any more (and we can run git revert --abort
to give up). Similarly, running git revert <hash-of-C>
will attempt to undo the correction. This will succeed, effectively reverting to revision B
!
This question, Undo a particular commit in Git that's been pushed to remote repos, is all about the backing-out kind of reverting. While that sometimes results in the reverting-to kind of reverting, it's not the same. What you want, according to your question, is more: "make me a new commit D
that has the same source code as commit A
". You want to revert to version A
.
Git does not have a user command to revert to, but it's easy
This question, How to revert Git repository to a previous commit?, is full of answers talking about using git reset --hard
, which does the job—but does it by lopping off history. The accepted answer, though, includes one of the keys, specifically this:
git checkout 0d1d7fc32 .
This command tells Git to extract, from the given commit 0d1d7fc32
, all the files that are in that snapshot and in the current directory (.
). If your current directory is the top of the work-tree, that will extract the files from all directories, since .
includes, recursively, sub-directory files.
The one problem with this is that, yes, it extracts all the files, but it doesn't remove (from the index and work-tree) any files that you have that you don't want. To illustrate, let's go back to our three-commit repository and add a fourth commit:
$ echo new file > newfile
$ git add newfile
$ git commit -m 'add new file'
Now we have four commits:
A <-B <-C <-D <-- master (HEAD)
where commit D
has the correct two-line README
, and the new file newfile
.
If we do:
$ git checkout <hash-of-A> -- .
we'll overwrite the index and work-tree version of README
with the version from commit A
. We'll be back to the one-line README
. But we will still have, in our index and work-tree, the file newfile
.
To fix that, instead of just checkout out all files from the commit, we should start by removing all files that are in the index:
$ git rm -r -- .
Then it's safe to re-fill the index and work-tree from commit A
:
$ git checkout <hash> -- .
(I try to use the --
here automatically, in case the path name I want resembles an option or branch name or some such; it makes this work even if I just want to check out the file or directory named -f
, for instance).
Once you have done these two steps, it's safe to git commit
the result.
Minor: a shortcut
Since Git actually just makes commits from the index, all you have to do is copy the desired commit into the index. The git read-tree
command does this. You can have it update the work-tree at the same time, so:
$ git read-tree -u <hash>
suffices instead of remove-and-checkout. (You must still make a new commit as usual.)