0

Here's what happened:

  1. Wrote code
  2. Did commit (commit: a)
  3. Code worked. There was much rejoicing.
  4. Decided to add features.
  5. Wrote more code, and created commits b, c, d, e, and f. These were pushed to the repo in master.
  6. Realized that was not the best way to do it, and we need to revert back to commit a, so, we did git checkout a and made changes, which work.

Now, we need to commit the code as-is to master, effectively deleting, ignoring, or jumping over commits b-f.

How do I tell git: "Yeah, I know I went back to commit a, and then made changes. I don't want anything in commits b, c, d, e, and f anymore. Just make commit g the code exactly as I have it now."?

DrDamnit
  • 4,736
  • 4
  • 23
  • 38
  • Were your changes in commits b,c,d,e, and f pushed to a repo? If these changes all occurred locally it's easy to erase those unwanted commits. – Matt Slonetsky Nov 03 '17 at 18:49
  • Yes. They were pushed to master and used for a couple months. (Updated my question above to reflect the answer to your question). – DrDamnit Nov 03 '17 at 18:51

2 Answers2

2

If you're OK with screwing up everyone who has used your pushed master, then use Mureinik's advice and just to git push --force origin.

However, if you have other people who use the master branch and you'd like them not to murder you in your sleep, then:

First, create a new branch. Let's call it "temp_work":

git checkout -b temp_work

This will keep your current commit.

Then, checkout master:

git checkout master

Revert commits b, c, d, e, and f:

git revert f e d c b

(It's in reverse order to avoid potential conflicts. Reverting the newest commits first should give you the lowest possibility of conflicts.)

You will get one editor screen for each commit to revert.

Finally, merge temp_work into master:

git merge temp_work

You can now delete the temp_work branch:

git branch -d temp_work

However, next time, avoid checking out an old commit and doing changes there. Revert the commits you don't want first, and then continue as normal from there, as it saves you a bit of work. But otherwise, still no big issue.

Nikos C.
  • 50,738
  • 9
  • 71
  • 96
  • git revert 55fa7605ec4594c217d4c135483bd9ab197af901 error: Commit 55fa7605ec4594c217d4c135483bd9ab197af901 is a merge but no -m option was given. fatal: revert failed – DrDamnit Nov 03 '17 at 19:06
  • Reverting merge commits requires a little more finesse, [more information here](https://stackoverflow.com/a/7100005/5074609) – Matt Slonetsky Nov 03 '17 at 19:11
  • @DrDamnit I fail to see what the problem is, if `git revert` need you to specify `-m` if you're reverting a merge, why are you just quoting the error message that tells you this with no more context? Did you not understand the error message? – Lasse V. Karlsen Nov 03 '17 at 19:12
  • @DrDamnit If the commits you want to revert are merges, then read this: https://git-scm.com/blog/2010/03/02/undoing-merges.html on how to revert them. – Nikos C. Nov 03 '17 at 19:14
  • I did not understand the error message, but played with it until I made headway (and RTFM). -m 1 got me back to where I needed to be. Commits reverted, new code added, merged, and happy. Thank you. – DrDamnit Nov 03 '17 at 19:18
  • Also, I see my comment did not fully paste into the comment above. It was missing: "Not sure what I need to revert, here. I assume it wants me to pick one of the branches that were merged?" – DrDamnit Nov 03 '17 at 19:19
  • @DrDamnit Merge reverts are a bit confusing, but it hits people often. Because of that, Git itself has a HOWTO for this, which will hopefully make it clearer what `-m` means: https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt – Nikos C. Nov 03 '17 at 19:33
1

There are multiple ways to do it. Which you should use depends on (a) what you want the history to look like, and possibly (b) whether the costs of a "history rewrite" make sense and (c) whether your answer to (a) or (b) is more important to you. So let's look at some options...

No matter what else you do, you may want to start by committing your new changes. This ensures that no amount of fiddling around will cause you to lose them.

git checkout -b temp
git commit

Now those new changes become G, and you have

A -- B -- C -- D -- E -- F <--(master)(origin/master)
 \
  G <--(temp)

When you're done, you'll delete temp, and if G isn't part of the actual solution state then it will get cleaned up eventually, so no harm in taking this first step as a safety net.

Now if you know you'll never care about B through F again, then the simplest solution with the cleanest result is to simply move master to G. This is a history rewrite, in that it removes commits from the history of a ref. When doing history rewrites, you have to coordinate with everyone else who uses the repo / might have fetched the ref when the "removed" commits were in its history. Things that would make a history rewrite more difficult to execute:

  • Other work has been done "on top of" F
  • Lots of developers use the repo
  • Some developers who use the repo are in limited communication and might not be immediately aware of what's happening

If you have any of those conditions, you might want to settle for a different solution. The ideal case for a history rewrite would be when you can coordinate with all repo users to

  • push all changes
  • discard all local repos
  • perform the rewrite
  • everyone re-clones

If you decide rewriting the history is the right thing to do:


First move any ref(s) pointed at F (e.g. master in the above example) to G

git checkout temp
git branch -f master

If other branches have been based on F, they need to be rebased to G. For example if you have

A -- B -- C -- D -- E -- F <--(origin/master)
 \                        \
  G <--(temp)(master)      H <--(feature_b)

then you could say

git rebase --onto temp origin/master feature_b

yielding

A -- B -- C -- D -- E -- F <--(origin/master)
 \
  G <--(temp)(master)
   \
    H' <--(feature_b)

If you have work based on B, D, or E (but not F), that could be more challenging, though rebasing that may also be ok.

Finally, you "force push" any branches you moved or rebased. e.g.

git checkout master
git push -f

At this point, all other users need to get back in sync. If, as I suggested above, everyone threw away their local repos, they can just re-clone and they're back in sync. Otherwise, any local changes they have, based on any commit you've replaced, need to be rebased; and their refs have to be updated. See "Recover from upstream rebase" in the git rebase docs for more detail.

Note that if all of this sounds good, except you want to keep the original commits B through F "off to the side" for future reference, then before moving master in the above procedure you can tag F.

git checkout master
git tag old_master

(In truth you can do it after moving master, but at that point it's a little harder to find F.)


If a history rewrite isn't good for your situation, then B through F have to remain in the history. You can then add one or more commits to "undo" B through F and apply G. This is still easiest if there aren't a bunch of branches all already based on F. In the simplest case, you can

git checkout master
git revert HEAD~4..HEAD
git rebase master temp
git checkout master
git merge temp

giving you something like

A -- B -- C -- D -- E -- F -- ~F -- ~E -- ~D -- ~C -- ~B -- G' <--(master)

If you want to jump straight from F to G' (without explicit commit(s) to revert changes), you could instead just re-parent G onto F (see git filter-branch docs). Or another way to do the same thing

git checkout temp
git reset --soft master
git commit
git checkout master
git merge temp

With any of these solutions, B through F still appear in the history (e.g. git log). On a repo-by-repo basis, if a developer wants to hide this they can supply A as a "replacement" for the commit before G' (i.e. G'^). See the git replace docs for details.

One additional option would be to merge G into master (presumably after a revert commit). This yields something like

A -- B -- C -- D -- E -- F -- ~FEDCB -- M <--(master)
 \                                     /
  ---------------- G ------------------

which is ok, but makes it harder to "paper over" the history with replacements. Note that in this case you should not combine the revert commit(S) with the merge commit, as this would produce an "evil merge" and could cause trouble down the line.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52