12

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.

Manthan Jamdagni
  • 894
  • 2
  • 17
  • 31
  • 3
    whats wrong with doing `git revert [commit]` then pushing it into a new commit? wont that revert commits back to A on local branch then you can push this new A into D – L_Church Mar 20 '18 at 15:29
  • Do you want commit `D` to have the _same_ change as `A`, or do you want `D` to revert the changes which `A` introduced? – Tim Biegeleisen Mar 20 '18 at 15:30
  • I want D to have the same code as A. I don't want to revert the changes introduced by A. – Manthan Jamdagni Mar 20 '18 at 15:33
  • 3
    @L_Church git revert A will revert the changes introduced by A but the changes introduced by B & C remain unaffected, I want to move to the code at A completely i.e. undoing all the changes done after A. – Manthan Jamdagni Mar 20 '18 at 17:30

2 Answers2

24

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.)

torek
  • 448,244
  • 59
  • 642
  • 775
4

Before starting, remember to git stash or backup all uncommitted files because they will be lost after these commands.

  1. git reset --hard <Commit A>
  2. git reset --soft <Commit C>
  3. git add . && git commit -m "Commit D"

Now it's at D and previous commits A, B and C have all been kept

dzcpy
  • 129
  • 5
  • 1
    I don't know why I've got a downvote, I guess maybe because it looks bit tricky. But this is the only easy way to achieve it. Please do some experiment yourself to see if it works or not before giving another downvote.. – dzcpy Mar 16 '20 at 12:09
  • I tried to follow the instructions at the accepted answer, but I either did something wrong, or something - the command `git read-tree` did nothing of what I wanted. Your solution *did* work, but it's a bad hack. (This really should be possible to do without moving the branch pointers around.) – Mike Rosoft Jun 02 '20 at 14:30
  • 1
    Perfect solution for my situation: a user had messed up their branch, and wanted to revert to a previous state, but they did not want to do a hard reset, and they wanted to preserve the entire git history, including the messed-up commits. Thank you! If you added some explanation on what's going on at each step, that would add even more value to this answer. – StoneThrow Jun 17 '22 at 17:22
  • For users that aren't afraid of `git reset --hard`, I think this is the best approach – Devin Rhode Jun 21 '23 at 20:21