F.P
F.P

Reputation: 17831

A merge was done wrong. How can I fix it?

There are two branches in our repo, release_1.18 and feature/remove-duplicate-media.

The Pull Request from feature/remove-duplicate-media ---> release_1.18 contained conflicts. The developer responsible for the feature merged release_1.18 back into his feature branch to resolve them (so far so normal).

History

But, upon this merge, he somehow (we can't quite figure out how) ditched ALL changes that were in release_1.18 and only committed the two files causing conflicts.

Now, the Pull Request feature/remove-duplicate-media ---> release_1.18 contains all the OPPOSITE of all changes between the two branches. So, a file that was added in release_1.18 is now removed.

How can we fix this? Merging release_1.18 again will not help, because git thinks (and it is right in this, technically) that this was already done and there is nothing left to do.

Upvotes: 2

Views: 106

Answers (2)

torek
torek

Reputation: 489083

TL;DR: we're going to repeat the merge no matter what

Marina - MSFT's answer—dropping the merge entirely—is one possible answer, and there are only two possible answers.

This is because, as you noted in the question, you can't get Git to "redo" the merge directly because it's already in the history. More precisely, the bad merge is the history. This means that to ditch (discard) a bad merge, you must rewrite history. Still, "ditch the bad merge" is one possible answer.

But there are really two parts to the ultimate answer to your question, too. One is: Which commit history alternative do you prefer? There are two of these: ditch and replace the bad merge with a good merge, or add a repair commit. The second part is: How do you get the source-code tree, i.e., the work-tree snapshot, that goes with the good merge or repair commit? It's this second question that's harder (unless you choose the "replace bad merge with good merge" method and are willing to take some time doing it). Let's see how we get there.

Alternative 1: replace the bad merge with a good merge

Since nothing currently depends upon the bad merge, "ditch the bad merge" is a relatively easy answer as well, at the moment. But it does mean that you'd have to get anyone who picked up the bad merge, to follow along with your "reset it away, then replace it with a good merge instead" by updating their own clones with that replacement.

Just to draw it yet again, here's a graphical representation of what we propose here:

...--o-------X   <-- feature/remove-duplicate-media
            /
...--o--o--o

(where X is the bad merge) becomes:

...--o-----------G   <-- feature/remove-duplicate-media
            \   /
             X /
            /_/
...--o--o--o

i.e., bad merge X no longer has any label pointing to it (after which it eventually gets garbage-collected away). Anyone who has git fetch-ed the bad merge X has it in their repository as remotes/origin/feature/remove-duplicate-media but if they run another git fetch before making new commits using commit X, they'll pick up good merge commit G, move their own remotes/origin/feature/remove-duplicate-media to point to good commit G, and they will be fine.

Alternative 2: add a "repair" commit

The other possible answer is to add, right after the bad merge, a good commit (merge commit or ordinary non-merge commit, it won't really matter). That is, instead of abandoning commit X, we add on a new commit—let's call it R for Repair rather than G for Good, just to distinguish it—that has, as its snapshot (its tree), what should have been in commit X after all. Assuming we make it as an ordinary commit, the graphical representation of commit R now looks like this:

...--o-------X--R   <-- feature/remove-duplicate-media
            /
...--o--o--o

Or, we can make R itself as a merge commit, in which case the graphical representation looks like this:

...--o-------X--R   <-- feature/remove-duplicate-media
            /__/
...--o--o--o

Is there any good reason for making R as a merge commit? Not really: it neither hurts nor helps. It's more a question of how you'd like to see it in your git log output. The real trick is: How will we obtain the tree for commit R?

How to get the tree for the good merge or repair commit

If you git reset away the bad commit X, it's easy to see how to get the good merge commit G: just run git merge again, resolve the merge correctly this time, and commit.

But if you're not going to ditch commit X—if you want or need to preserve the existing history—how will you get the same kind of tree for repair commit R?

The answer is almost eerily simple, once you realize how Git works: you make the tree for repair commit R the same way you make commit G itself, by repeating the merge. Obviously you must do this on a branch that does not have bad commit X, but that's almost hilariously easy!

Let's draw our G case again but with one change: let's leave bad merge X in place, with the feature branch pointing to it. This time I'll label two more commits as well, A and B:

...--A-----------G   <-- temp
            \   /
             X /   <-- feature/remove-duplicate-media
            /_/
...--o--o--B

How do we get this temporary branch temp and make this new merge G? Simple: just check out a new branch temp pointing to commit A, then merge commit B. Commit A is the first parent of bad merge X, and B is the second parent of bad merge X, so the recipe is:

$ git checkout -b temp feature/remove-duplicate-media^1
$ git merge feature/remove-duplicate-media^2

Then resolve and commit as before, and we have good commit G, as the tip of branch temp.

Now we just need to copy G to repair-commit R, on branch feature/remove-duplicate-media. Normally we might use git cherry-pick to copy a commit, but that's not suitable here. There are two ways to do the copying, using either regular old Git commands, or using a plumbing command, git commit-tree.

The plumbing command is actually easier and more flexible. It will allow us to copy G to either an ordinary, single-parent commit, or to a new merge commit. To make R, we prepare a log message:

$ cat > /tmp/log-msg
(write your log message here, ^D to exit cat)

or:

$ vim /tmp/log-msg

or however you like to make log messages. Now make the commit, on no branch at all, just as a dangling commit object:

$ newid=$(git commit-tree -p feature/remove-duplicate-media \
>   -F /tmp/log-msg temp^{tree})

or:

$ newid=$(git commit-tree -p feature/remove-duplicate-media \
>    -p feature/remove-duplicate-media^2 \
>    -F /tmp/log-msg temp^{tree})

and then fast-forward branch feature/remove-duplicate-media to the new commit:

$ git checkout feature/remove-duplicate-media
$ git merge --ff-only $newid

(note: this may force you to remove some currently-untracked files if they are tracked in the corrected merge, although it's a bit unlikely).

Since $newid's first parent—maybe its only parent, depending on which command you used above—is the current tip of feature/remove-duplicate-media, the merge will be a fast-forward and you'll now have, as the tip of feature/remove-duplicate-media, the new commit you just made by copying the temporary commit. Now you can delete the temporary commit's branch name:

$ git branch -D temp

Just for completeness, how to copy temp with ordinary Git commands

If you just want to make commit R as an ordinary single-parent commit, and want to use "ordinary" Git commands, here's how to do that:

$ git rev-parse --show-cdup

If this prints any number of "../"s, like "../../../" or some such, cd that many ../-es so that re-running the command prints only a blank line. (The idea here is to get to the top level.) Or:

$ cd $(git rev-parse --show-toplevel)

will do the same thing. Then:

$ git checkout feature/remove-duplicate-media
$ git rm -rf .
$ git checkout -f temp -- .
$ git commit

The first git checkout gets on the target branch and populates the index and work-tree. The git rm -rf . removes everything—literally everything—that's in the index, and all the corresponding work-tree files. The second git checkout re-fills the index and work-tree from the temporary merge, undoing the effect of the git rm -rf . (except that file timestamps wind up being updated). The final git commit commits the result.

We can, as noted before, hit minor issues with files in temp that are untracked in HEAD. That's what the -f in the second git checkout is for. There probably are no such files and the -f is then unnecessary. You can leave it out, and if you hit such files, remove them (or hide them away) manually.

There's a similar method that avoids the slightly scary (and time-stamp-changing) git rm -rf . step, but it's a bit more complicated. Instead of removing everything just to repopulate everything, we can use git checkout temp -- . to update everything, after first removing any files in the current commit that are not in the temp commit:

$ git diff --no-renames --name-only --diff-filter=R -z HEAD temp | \
> xargs -0 git rm -rf --
$ git checkout -f temp -- .

(you can do this without xargs, but then you need to skip -z and you can run into issues with filenames that contain embedded white-space). Note that we must do this removal first, in case there are file names that correspond to directory names in the temp commit.

As before, once you have the temporary merge copied to the committed repair-commit R, you can simply delete the branch temp.

Upvotes: 1

Marina Liu
Marina Liu

Reputation: 38116

You mean the merge from feature/remove-duplicate-media to release_1.18 wrongly merge opposed.

  A---B---C         release_1.18
 /         \
D---E---F---G       feature/remove-duplicate-media

So you can drop this merge by:

git checkout feature/remove-duplicate-media
git reset --hard <commit id for F>

Now both the two branches will stay the status just as before merge. Then you can merge again.

Upvotes: 2

Related Questions