30

A merge commit is a commit with at least two parents. These parents are in specific order.

If I'm currently on the branch master, and I merge in the branch feature, I create a new commit with its first parent being the commit from master, and the second commit being the commit from feature. This order is especially evident by running git log --first-parent.

*   The merge commit
|\
| * The commit from `feature`
* | The commit from `master`

Say I now realise that the order is the wrong way round: I intended to merge the branch master into feature by running git checkout feature; git merge master. I want to swap the order of the parents of a merge commit, but I do not want to go through the hassle of resolving all the merge conflicts again. How can I do this?

*   The merge commit
|\
* | The commit from `feature`
| * The commit from `master`
Flimm
  • 136,138
  • 45
  • 251
  • 267

5 Answers5

39

Actualy, there's a really cool command I learned recently that will do exactly what you want:

git commit-tree -p HEAD^2 -p HEAD^1 -m "Commit message" "HEAD^{tree}" 

This will create a new commit based on what is currently HEAD, but pretend that it's parents were HEAD^2,HEAD^1 (note this is the reversed order).

git-commit-tree prints the new revision as output, so you might combine it with a git-reset-hard:

git reset --hard $(git commit-tree -p HEAD^2 -p HEAD^1 -m "New commit message" "HEAD^{tree}")
Jared Grubb
  • 1,139
  • 9
  • 17
  • 4
    I think you should use `--soft` instead—or at the very least omit the `--hard`. No content depends on the order of the parents, it's just an ancestry question. Inflight content changes are still just as good, there's no reason to reset those. – jthill Oct 31 '16 at 19:34
  • 1
    Thanks for sharing this cool command, it was very useful to me. I like to do the `git commit-tree` separately, then copy the returned SHA-1 and do a `git checkout` on it (unless my HEAD points to a branch which I'd like to put at the new commit anyway, then I do a reset). Additionally I like to double-check if the old and the new commit really point to the same tree, using `git rev-parse fa1afe1^{tree}` (replace `fa1afe1` with the old or the new commit id respectively) – msa May 16 '19 at 16:34
7

One way would be to simulate a merge. So how can we do that?

Lets assume you have the something like following commit graph:

* (master) Merge branch 'feature'
|\
| * (feature) feature commit
* | master commit
. .
. .
. .

Keep the changes

We want to merge master into feature but we want to keep the changes, so at first we switch to master, from which we "manually" update our HEAD reference to point at feature while not changing the working tree.

git checkout master
git symbolic-ref HEAD refs/heads/feature

The symbolic-ref command is similar to git checkout feature but doesn't touch the working tree. So all changes from master remain.

Undo the old merge

Now we have all changes from the merge in the working tree. So we continue with "undoing" the merge by resetting master. If you don't feel comfortable loosing the reference onto the merge commit you can create a temporary tag or branch.

(If you want to keep the commit message, now is a good time to copy it somewhere save.)

# Optional
git tag tmp master

git branch -f master master^

Now your commit tree should look just like before the merge.

Fake the merge

And here comes the hacky part. We want to trick git into believing that we are currently merging. We can achieve this by manually creating a MERGE_HEAD file in the .git folder containing the hash of the commit we want to merge.
So we do this:

git rev-parse master > .git/MERGE_HEAD

If you are using a git bash, git will now tell you that it is currently in the process of merging.

To finish our merge we just have to commit.

git commit
# Enter your commit message

And it's done. We recreated our merge commit but with swapped parents. So you commit history should now look like this:

* (feature) Merge branch 'master'
|\
| * (master) master commit
* | feature commit
. .
. .
. .

If you need any further information don't hesitate to ask.

Sascha Wolf
  • 18,810
  • 4
  • 51
  • 73
7

Inspired by this answer, I came up with this:

git replace -g HEAD HEAD^2 HEAD^1 && 
git commit --amend && 
git replace -d HEAD@{1}

The first commands switches the two parents in something called a replacement ref, but only stores it locally, and people have called it a hack.

The second command creates a new commit.

The third command deletes the older replacement ref, so it doesn't mess up the other commits depending on that commit.

Lena
  • 350
  • 4
  • 13
  • 1
    @nitzel It's what I use to fix them, although you might want to edit the commit description if you use an auto-generated commit description when you normally merge the code. – Lena Oct 22 '19 at 20:43
  • Since @nitzel's comment was deleted, and for the context: we were talking about fixing foxtrot-merges. – Lena Feb 03 '20 at 16:32
2

Update - It's never that easy, is it?

I've recognized a flaw in the original instructions I suggested: when doing a checkout with path arguments, you might expect a file to be removed from your working path because it isn't in the commit you're checking out; but then you might be surprised...


As with any history rewrite, it's worth noting that you probably shouldn't do this (using any of the methods form any of these answers, including this one) to any merge you've already pushed. That said...

The previous answers are fine, but if you'd like to avoid using (or needing to know) plumbing commands or other git inner workings - and if that's more important to you than having a one-liner like Jared's solution - then here's an alternative:

The overall structure of this solution is similar to zeeker's, but where he uses plumbing commands to manipulate HEAD or "tricks" git into completing a merge, we'll just use porcelain.

So as before we have

* (master) Merge Commit
| \
| * (feature) fixed something
* | older commit on master

Let's begin:

1) Tag the old merge

We'll actually be using this tag. (If you want to write down the abbreviated SHA1 instead, that'll work; but the point here is making the fix user friendly, so...)

git tag badmerge master

Now we have

* (master) [badmerge] Merge Commit
| \
| * (feature) fixed something
* | older commit on master

2) Take the merge out of master's history

git branch -f master master^

Simple enough:

* [badmerge] Merge Commit
| \
| * (feature) fixed something
* | (master) older commit on master

3) Start a new merge

git checkout feature

If we just run git merge master now, we know the merge will fail; but we don't want to redo the manual conflict resolution. If we had a way to overlay the data from badmerge onto our new merge commit, we'd be all set... and we do!

To start the merge (a) without creating conflict state to be cleaned up, but (b) leaving the merge commit open so we can "fix" it:

git merge --no-commit --strategy=ours master

4) Overlay the already-resolved/merged data from badmerge

Making sure we're in the root directory of the repo we might then do git checkout badmerge -- . (note that we've provided a path (.) to git checkout, so it only updates the working tree and the index); but this actually can be a problem.

If the merge resulted in files being deleted (but they're in our working tree right now), then the above command won't get rid of them. So if we're not sure, we have a couple options to be safe from that possibility:

We could first clear our working tree... but we do need to be careful about not wiping out the .git folder, and may need special handling for anything in our ignore file.

In the simple case - .git is the only .-file, nothing being ignored - we could do

rm -rf *
git checkout badmerge -- .

Or, if that seems too risky, another approach is to skip the rm, do the git checkout badmerge -- ., and then diff against badmerge to see if anything needs cleaning up.

5) Complete the merge

git commit
git tag -d badmerge
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
1

You may want to use git replace --graft (ex. git replace --graft HEAD HEAD^2 HEAD^1) instead of rewriting history. It creates a replacement commit and keeps its children unchanged.

Edit: Created replacements are not shared by default. You need to configure remotes like push = refs/replace/*:refs/replace/* and also tell your contributors to set fetch = refs/replace/*:refs/replace/*.

See also:

snipsnipsnip
  • 2,268
  • 2
  • 33
  • 34
  • Problem with this is that graft replacement refs are only stored locally, which can be confusing and is probably not what the author wants. I wrote an slightly different answer [here](https://stackoverflow.com/a/56610502/4633439). – Lena Jun 15 '19 at 12:47
  • 1
    Thank you for pointing out the problem with sharing. Added the note. – snipsnipsnip Jun 20 '19 at 00:48
  • I don't know, even with that note, it's still kinda a hack. I think a lot of contributors are going to forget to set that, and even if they don't, it's still kinda unnecessary extra work they have to do. – Lena Jun 27 '19 at 10:29