How do I swap the order of two parents of a Git commit?

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}")

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.


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.


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

Tags:

Git