Is there a way to revert to a certain commit without changing the remote history, basically undoing all the changes from that commit in a new commit just like git revert?
For eg - I have 3 commits commit A -> B -> C, my head is currently at C I want to now create another commit D which will have the same code as A so that I can push that commit to the remote branch without changing history.
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
.
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.
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.)