2

I read How I reverted several git commits in a single commit, and that's exactly what I want as the result. However, I do want to revert all in one commit because 1) there're many commits to be reverted 2) it's likely that I need to revert the reverts later.

I got luckier than the example in the link, as there weren't commits to be retained in the middle of those to be reverted. This is what my history looks like:

A–C1–C2–C3–C4–O1–O2–O3

where the four C commits are the ones I want to revert, and the O commits were made by other authors later.

How can I revert the four C's in one commit R, which can be reverted later to have all C's in effect again?

3
  • If the O commits were made by others, changing the C commits means you are rewriting the history of the repository. This is never recommended (specially if the repository is public). Are you sure that is that what you want to do? Commented Jul 7, 2015 at 3:42
  • @LucasSaldanha I'm not changing the C commits. I just want one R commit at the end of history that says "revert all C commits". And the O commits are irrelevant to the C commits. Commented Jul 7, 2015 at 3:43
  • Ah now I get it. I believe that @durek gave you a correct answer. :) Commented Jul 7, 2015 at 10:19

1 Answer 1

4

Like so many git commands, git revert uses git rev-list (or the moral equivalent thereof) to parse its arguments: see in particular the description of OPTIONS. This means, among other things, that you can simply pass a range of commits, as spelled out in the gitrevisions documentation. Remember that C1..C4 excludes C1 itself1 so if you're using this form, write C1^..C4 or A0..C4.

That takes care of specifying which commits to revert, but still leaves you with a revert that makes four new commits:

$ git revert 22b38d1^..676699a

does not do what you want.2 What you want is also listed in those OPTIONS, and is just -n (or --no-commit, same thing):

$ git revert -n 22b38d1^..676699a
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
You are currently reverting commit 22b38d1.
  (all conflicts fixed: run "git revert --continue")
  (use "git revert --abort" to cancel the revert operation)
[remainder snipped]

All four commits are now reverted but no commit has been done so now I must manually commit the result. If I run git revert --continue git will make a commit for me, but will only mention the first reversion (22b38d1, in this case—the reversion is done in reverse, with 676699a done first in case that's needed to revert the previous commit, and so on).

$ git commit

This brings up the editor with a "reverts (the first commit)" message, which I can now fix to say "revert 22b38d1^..676699a in one swoop". (Constructing a good reversion message can be difficult; git's default for automatic commits is adequate, but the default in this case, with -n, is not.)


Git being git, of course, you can use this same technique without the -n and with the git rebase -i mentioned in your linked posting: squash together the four reverts into one single revert, but use the squash action to combine the commit texts. Here you might run:

$ git revert --no-edit 22b38d1^..676699a
[messages omitted]
$ git rebase -i HEAD~4
[editor runs: change last three to "squash", write, exit editor]
[editor runs again: edit commit message to describe 4-in-1 reversion]

You might prefer this second method (I think I prefer it myself, it's just not quite what you asked for).


A side note on this: git rebase simply copies (as if by cherry-picking) commits, using temporary, anonymous branch. To use your notation, you start with:

A – C1 – C2 – C3 – C4 – O1 – O2 – O3   <-- branch

and acquire, after the four reverts (without -n, making four new commits), this:

                                     R4 - R3 - R2 - R1   <-- branch
                                    /
A – C1 – C2 – C3 – C4 – O1 – O2 – O3

where the R's are the reversions of the C's. The rebase then copies the original R's, tacking the copy (which, in this case, ends up being just one commit which I'll call R) on at the point the rebase starts-from, and moving the branch label so that it points to the last of the new commits (making the temporary branch permanent again):

                                     R4 - R3 - R2 - R1   [abandoned]
                                    /
A – C1 – C2 – C3 – C4 – O1 – O2 – O3 - R   <-- branch

What happens to R1 through R4? They've been abandoned and are eventually3 garbage-collected.


1Unless you use --boundary, but that marks the boundary commits with - in git rev-list output. I have not tested this with git revert: the hat suffix to move back one commit in the graph is sufficient, and is what I generally use for these cases. (It fails if you reach a root commit but that's not going to happen here.)

2Not just because those particular commit hashes are unique to the repository I ran these commands in, either. Your hashes will be different; or you can use relative names like HEAD~6^..HEAD~3 or equivalently HEAD~7..HEAD~3. This assumes your drawing of the commit graph fragment is accurate and you haven't done anything since then to move HEAD.

3The old commits are still find-able via the reflogs for HEAD and for your branch. This keeps them around for 30 days by default, after which the old reflog entries expire and are deleted and then a git gc (including any automatic ones git does as needed) will remove those commits.

1
  • Thank you so much for such a thorough comparison between the options! Super helpful. Commented Jul 8, 2015 at 18:03

Not the answer you're looking for? Browse other questions tagged or ask your own question.