34

Say I have four commits A-->B-->C-->D but C was terribly misguided. How can I undo C without losing the changes from D? I understand the basic functionality of git-revert and undoing commits, but I can't figure out how (if it is even possible) to undo a specific commit (not the latest) without having to redo the changes that came after it.

Community
  • 1
  • 1
amflare
  • 4,020
  • 3
  • 25
  • 44
  • 2
    That's what `git revert` does: it reverse-applies (i.e., reverts) some commit. Identify the bad commit `C`, run `git revert `, and Git applies a new commit to `HEAD` that undoes whatever was done in `C`. (This is in direct contrast to Mercurial, where `hg revert` means "restore some file(s)" and you use `hg backout` to undo a specific commit. Same revert verb, entirely different meaning.) – torek Jan 11 '17 at 21:01
  • @torek - Wait, so it does not revert _to_ the specified commit (ie backup the history until that commit)? It only undoes the specified commit? – amflare Jan 11 '17 at 21:03
  • 1
    Exactly. In Git, that is. If you switch back and forth between Git and Hg it's really easy to goof this up (he says from experience :-) ). (BTW I think Hg's "backout" verb is better here; the problem with the verb "revert" is precisely what you've identified: is it "revert foo" or "revert TO foo"?) – torek Jan 11 '17 at 21:04
  • @tbirrell just be aware C will still be in your commit history, as well as the new revert commit. You're history willnot look like: `A->B->D`, but `A->B->C->D->E` – Ray Jan 11 '17 at 21:13
  • FWIW, the way to revert _to_ a specified commit in git is `reset`. – Dan Lowe Jan 11 '17 at 21:35

2 Answers2

29

Revert

The git revert command is designed to do exactly this.

git revert <hash for C>

This will create a new commit which reverses the change in C.

Rewrite History

You can also rewrite history. This is not generally recommended, and should only be used if you have a good reason to actually remove a commit from history (say, if it contains a password or something).

git rebase -i <hash for B>

In the editor, just delete the line with the hash for C. The usual caveats apply if you have already pushed.

Non-interactive

Technically, all of these options involve some kind of merge resolution which means they cannot truly be non-interactive. The only difference is the resulting history. With git revert you get a history that looks like this:

A -> B -> C -> D -> E
                    ^
                    +--- reverts C

With git rebase, you end up with a history that looks like this:

A -> B -----------> E

You can of course just do git rebase --onto B C, instead of git rebase -i, but this is still an interactive process because you have to manually resolve any merge conflicts that can't be automatically resolved by the merge strategy.

Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
  • also should note not to use rebase -i to reapply commits that have already been made public and pulled by others – jbu Jan 11 '17 at 23:32
  • If tiberrell has already pushed his changes, then I think Ray's solution is better as it doesn't destroy history. If the changes are only local, then rebase can also work in fewer commands (though is more prone to user error) – jbu Jan 12 '17 at 01:56
  • 1
    @jbu: Actually, that's incorrect—Ray's solution does destroy history. It produces the same result as `rebase -i`, it just takes a roundabout path to get there. The way to preserve history is with `git revert`. – Dietrich Epp Jan 12 '17 at 05:54
  • you're right, I didn't read his commands carefully @DietrichEpp +1 – jbu Jan 12 '17 at 06:50
  • Is there an answer that doesn't rely on -i interactive. An answer that "just does it"? – PandaWood Jul 28 '18 at 04:15
  • @PandaWood: Yes, `git revert` "just does it" and that's what you should be doing, it's already mentioned in the answer. You can do the equivalent `git rebase` operation as well, it's just more cumbersome and dangerous than the interactive version because with the non-interactive version, you have to copy and paste the right commit hashes or write `HEAD~2` `HEAD~1` correctly or you end up with missing bits of history, with `-i` it's much better because you can see the commit messages when you are manipulating them. – Dietrich Epp Jul 28 '18 at 20:30
  • On Windows Git Bash, I ran `git rebase -i HEAD~2`, which then opened up a prepopulated commit message in Notepad++, and I was confused about what to do. Eventually I realized that I could cut and paste the 2 top lines to reorder the commits. When I closed Notepad++, the rebase finished (since there were no conflicts), and my commits were reordered. Then I ran `git reset --soft HEAD~1` to uncommit the now most recent commit (moving them back to staged). – Ryan Dec 16 '18 at 01:53
3

Assuming:

  • A, B, C, and D are commit hashes (replace them with the hash wherever you encounter them)
  • You actually want commit C gone from history permanently from master branch, not masked by a 'revert' commit. If not the case, just use git revert C.

General steps

  • Create a new branch off the main at commit B (this branch should look like A->B)
  • Cherry pick the commit D to the new branch. (it should now be A->B->D') it will get a new hash, I'll call it D'
  • Switch to main branch
  • Delete the C & D commits from the main branch ( main would look like A->B - Merge your new branch into main (main should now be A->B->D')
  • Delete your test branch.

Here's the code:

git checkout -b temp B
git cherry-pick D   
git checkout master
git reset --hard HEAD^^
git merge temp
git branch -D temp
Ray
  • 40,256
  • 21
  • 101
  • 138
  • This process looks like a manual version of `git rebase -i ` – Dietrich Epp Jan 11 '17 at 21:13
  • 1
    @DietrichEpp I get nervous doing rebases :) – Ray Jan 11 '17 at 21:17
  • This way you get the complete branch in the temp to look at. I guess you could rebase on the temp branch first and verify. – Ray Jan 11 '17 at 21:19
  • You're still doing a rebase either way, but you're doing it manually with 6 steps (maybe you forgot one?) instead of automatically with 1 step. – Dietrich Epp Jan 11 '17 at 21:22
  • I get your point. The only reason to be able to see the final result before changing the master branch, but you could do the rebase first on a test branch, verify it's as you desire, then do it on master. – Ray Jan 11 '17 at 21:35