Search code examples
gitgithubgit-revert

git revert remote to a particular commit with a merge doesn't work with -m


I'm trying to revert back our remote repo to a previous commit. The tree is as shown below:

enter image description here

The idea is that we want to go back to that commit without requiring anyone who's pulled from master branch to have to deal with the problems that would arise from resetting back to commit 2dda031. So I'm using git revert instead.

I'm trying to do this by using git revert --no-commit 2dda031..HEAD

However, I get this error:

error: commit d064f7c3b04a2bda30c43a32afac822c6af633c0 is a merge but no -m  option was given. 
fatal: revert failed

This is expected as d064f7c is a merge (so is 47d4161). So as suggested here I do:

git revert --abort
git revert --no-commit -m 1 2dda031..HEAD

I then get the error message:

error: mainline was specified but commit cb420e0 is not a merge.
fatal: revert failed

So I just feel like I'm going in cycles. Can someone show me the correct way of reverting back to that commit (while restoring the history)?


Solution

  • Edit (original answer below): let me start by converting your graphic to text, with (I hope) no typos or other drastic mistakes. This is what you have now, as git log --graph --oneline might show it (though --graph --oneline might choose a slightly different commit ordering—the blue and green lines generated by your graphical viewer are probably sorted by commit date without regard to topology, instead of being sorted by topology first):

    * cb420e0  (master, ...) evert "Update README.md"
    * 7a16df4  Update README.md
    * 7564754  Update README.md
    * 214cd47  Update README.md
    * d064f7c  Merge pull request #6 from ...
    |\
    * | d936a24  Changing Run instructions
    * | 2cbd7c2  Minor edits for Google Drive link
    * | 1a3d871  Updated process documentation with google drive link
    | * 0594132  (TrustM..., ...) Added some comments to various scripts.
    | * 7e060c4  Updated the JSON dialogues and implemented the Trust mechanism
    |/
    * 4d7f49b  Configured script inputs and enabled mouse during pause screen.
    * 47d4161  Merge pull request #5 from ...
    |\
    | * e999b3d  (origin/Trying...) Adjusted ray cast length to be more realistic.
    | * 953e4c3  Fully functional dialogue system implemented.
    * | 1f33079  updated wiki to reflect marking of prototype
    | * 09e350b  Added in most of the Yarn framework
    | * 2dda031  fixed heirarchy of files
    | * bf667cc  Merge branch 'develop' of ...
    | |\
    | * |  79e068d  Character placement
    

    (and we cannot see anything below this point, though obviously there must be many more commits).

    The commit state that you wanted to get back to is, I guess, 2dda031 fixed heirarchy of files.

    Now, the trickiest part of this is that this state "lives on" what was apparently a side branch, under the "Merge pull request #5 from ..." commit. If at some point during the revert process, you were to run git revert -m <some-number> 47d4161, you would be telling Git to diff 47d4161 against either its first parent, 1ff3079, or against its second parent, d999b3d. The first of these diffs shows the effect of every commit since the merge base—whatever commit that is: we cannot see it from this fragment of the graph; we need more graph to find it, as it's off the bottom of the "screen" here—to one of these two, and the other diff shows the effect of every commit since the merge base to the other. So reverting with -m 1 basically takes away the effect of:

    * e999b3d  (origin/Trying...) Adjusted ray cast length to be more realistic.
    * 953e4c3  Fully functional dialogue system implemented.
    * 09e350b  Added in most of the Yarn framework
    * 2dda031  fixed heirarchy of files
    * bf667cc  Merge branch 'develop' of ...
    ...
    

    while using -m 2 basically takes away the effect of:

    * 1f33079  updated wiki to reflect marking of prototype
    ...
    

    (in both cases there may be many more commits that we cannot see here). It seems pretty clear to me that you don't want to do that, but I don't know for sure.

    Note, however, that if you simply extract the contents of commit 2dda031, you still lose the effect of 1f33079 and whatever other commits may be below it, because you're getting the state as of a few commits before origin/Trying....

    Things are simpler with respect to commit d064f7c, because that simply merges d936a24 and 0594132 from merge base 4d7f49b. If you wanted to undo the effect of commits 1a3d871+2cbd7c2+d936a24, you could git revert -m 1 d064f7c. If you wanted to undo the effects of commits 0594132+7e060c4, you could git revert -m 2 d064f7c. But since you (presumably) want to undo the effects of all of these, it's simpler just to undo them all individually, skipping the merge entirely.

    Since you presumably also want to undo cb420e0 back through 214cd47, you would just undo those individually.

    If you want to keep the effect of 1f33079 and earlier commits, simply don't revert them. If you want to undo their effect, you can git revert -m 2 47d4161, which will undo them all at once. I doubt you want that, but as before, it's up to you.

    Note that any revert can be run with -n (revert in index and work-tree without committing), but once you've started a series of -n operations, you must continue with -n and eventually commit, before you can start a new revert without -n.

    Perhaps the simplest approach, depending on the result you want, is to start by extracting the contents of commit 47d4161—the tree state from as far back as you can go without doing individual commit-backouts—and then use git revert -n on each additional commit to revert. To extract those contents, you can either use git checkout <commit> -- . with its slight risk of not removing index and work-tree entries for files that are new since then, or git read-tree --reset -u <commit> to avoid the risk (see remarks below, in original answer, too). I would go with the latter, giving:

    git read-tree --reset -u 47d4161
    git revert -n e999b3d
    git revert -n 953e4c3
    git revert -n 09e350b
    

    (assuming that gets you the final work-tree and index states that you would like to have, of course).

    (Original answer below line.)


    Git's revert does not revert to a commit (which makes the command poorly named: it uses the wrong verb). What it does is back out (i.e., "revert") one particular commit, or possibly a set of commits.1 You're partway there with 2dda031..HEAD, since this range syntax actually means HEAD ^2dda031, i.e., the set of all commits reachable from HEAD, excluding (subtracting away, with set-subtraction) the set of all commits reachable from 2dda031.

    Now, there are multiple problems here, because of the merges. The first is that excluding 2dda031 and its parents fails to exclude the other leg of the merge, so you'll be reverting far too many commits. The second problem is that, in a sense, a merge commit is a commit that makes, as its single-commit change, "all of the changes being brought in by a side branch".2 The third is that in order to revert a merge commit, you must specify which "side" to consider, but in order to revert a non-merge, you must not specify any "side".

    A solution to some of these problems is to avoid reverting merges at all, and a solution to others is to revert only the merge, where applicable. But there's yet another, easier way, depending on your actual goal: if it's truly to revert to a commit, the Git verb for doing this is actually git checkout—but there are some traps. See this answer to a related (possibly even duplicate, depending on your goal) question. The reason for the git rm -r . is to remove any files in the current index that are not going to be extracted by the git checkout <hash> -- . step.

    There is a short-cut you can use instead of the git rm -r . && git checkout <hash> -- . sequence, that also does not depend on the current working directory: you can run git read-tree --reset -u <hash>. This discards the current contents of the index (--reset) and replaces them with the contents of the specified commit (the <hash> argument), and then updates the work-tree to match, removing any files removed from the index and updating any files updated in the index.

    Note that in all cases, the end result is in the index and work-tree, but is not yet committed, so you must run git commit.

    Note that if your goal is not, in fact, to revert to a specific commit, but rather to revert a series of changes brought in, the way to do that with git revert -n is to use as many separate git revert commands as required: one for each change-set to be backed out. Some of these may be git reverts of non-merges and some may be git reverts of merges. See footnote 2, though, and remember no matter which method you use, backing out a change that you wanted to keep after all will succeed (the change will be gone), even though you wanted to keep it.


    1For this reason, the verb in some other VCSes is "backout".

    2This description is wrong in a subtle but very important way: merging combines changes. The input to a merge is two sets of changes: one from a merge base commit to an --ours commit, and the other from a merge base commit to a --theirs commit. These two change-sets may overlap. If they do, and if the overlap is "sufficiently similar" at any point, Git takes just one copy of the change. If Git has taken one copy of some change ∆, where ∆ appears in both change-sets, and you revert the --theirs change-set, Git backs out ∆ even though it was in the --ours change-set too. (The same reasoning applies if you back out the --ours delta: Git should not, but does, take it away from the version brought in as --theirs.)